Bài 7 · Vận dụng · 24 phút
Lưu dữ liệu với PostgreSQL
Biên soạn bởi Nguyễn Anh Tuấn
Cho sản phẩm biết nhớ thông tin: thiết kế mô hình dữ liệu cho web bán hàng - tài khoản, sản phẩm, kho, đơn hàng, ví/số dư và lịch sử giao dịch - lưu vào PostgreSQL qua Drizzle ORM (mô tả bảng bằng TypeScript, type-safe), nối vào app Next.js ở mức khái niệm; chuỗi kết nối để trong biến môi trường (env), không hardcode. Trung thực về quyền riêng tư và dữ liệu người dùng - đủ để ship mà không gây hại.
Khung web ở các bài trước có một điểm yếu: tải lại trang là quên hết. Thêm một sản phẩm, reload, nó biến mất. Vì mọi thứ mới nằm tạm trong bộ nhớ lúc chạy, không được ghi lại đâu cả. Một cửa hàng thì phải có sổ sách: nhớ khách là ai, còn bao nhiêu hàng, ai đã mua gì.
Cuốn sổ đó là database (cơ sở dữ liệu), và khoá dùng PostgreSQL. Bài này không bắt mèo con viết database; bài này dạy hình dung dữ liệu để mô tả cho Claude cho đúng.
Database quan hệ lưu dữ liệu thành bảng - giống một cuốn sổ kẻ ô. Mỗi bảng giữ một loại thứ (sản phẩm, người dùng…); mỗi hàng là một bản ghi (một sản phẩm cụ thể); mỗi cột là một thuộc tính (tên, giá, tồn kho).
bảng san_pham - câu lệnh SQL minh hoạ (Claude viết hộ; mèo con hiểu đại ý)
CREATE TABLE san_pham (
id SERIAL PRIMARY KEY, -- số thứ tự tự tăng, định danh mỗi dòng
ten TEXT,
gia INT,
ton_kho INT
); - ▸Database quan hệ lưu dữ liệu thành bảng (hàng = bản ghi, cột = thuộc tính).
- ▸Mỗi bảng giữ một loại thứ: san_pham, nguoi_dung, don_hang…
- ▸Cột id là số định danh độc nhất cho mỗi hàng.
Một web bán hàng có ví cần vài bảng, và chúng nối với nhau qua cột id. Mỗi người dùng có một ví; mỗi đơn hàng thuộc về một người dùng và một sản phẩm:
Cột kiểu nguoi_dung_id trong bảng vi chính là cách nói "ví này của người dùng số mấy" - dân database gọi là khoá ngoại (foreign key). Nhờ vậy dữ liệu không phải chép đi chép lại: thông tin khách nằm một chỗ, các bảng khác chỉ trỏ tới.
PostgreSQL là phần mềm giữ các bảng đó. Phần backend của Next.js nói chuyện với nó: cần hiện danh sách hàng thì đọc bảng san_pham; khách đặt mua thì ghi một dòng vào don_hang. Mèo con mô tả việc, Claude viết câu truy vấn và nối hộ.
Trung thực
Viết SQL tay dễ sai: gõ nhầm tên cột, lệch kiểu dữ liệu, tới lúc chạy mới lòi ra. Một ORM (Object-Relational Mapping) là lớp trung gian: mèo con mô tả bảng bằng TypeScript, ORM lo sinh SQL và kiểm kiểu giúp. Khoá chọn Drizzle ORM vì nó nhẹ, cú pháp gần SQL nên dễ đọc, và type-safe - gõ sai tên cột là báo lỗi ngay khi code.
db/schema.ts - mô tả bảng san_pham bằng TypeScript (Drizzle); Claude viết hộ, mèo con đọc lại
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
// Mô tả bảng san_pham - đây là "bản vẽ", Drizzle sinh SQL từ nó
export const sanPham = pgTable('san_pham', {
id: serial('id').primaryKey(),
ten: text('ten').notNull(),
gia: integer('gia').notNull(), // đơn vị đồng (số nguyên)
tonKho: integer('ton_kho').notNull().default(0)
});
// Đọc danh sách sản phẩm - query được kiểm kiểu, gõ sai cột là báo lỗi
const ds = await db.select().from(sanPham); - ▸ORM = lớp trung gian: mô tả bảng bằng TypeScript, ORM sinh SQL và kiểm kiểu.
- ▸Drizzle: nhẹ, cú pháp gần SQL, type-safe - hợp Next.js + TypeScript của khoá.
- ▸Schema là một file TypeScript trong repo; Drizzle sinh migration để tạo/đổi bảng an toàn.
Trung thực
Để nói chuyện với database, app cần một chuỗi kết nối (connection string) - gồm địa chỉ, tên database, user và mật khẩu. Đây là bí mật, nên tuyệt đối không ghi thẳng vào code: code đẩy lên GitHub là ai cũng đọc được mật khẩu. Chỗ của nó là file .env (biến môi trường):
.env - giữ bí mật ngoài mã nguồn (minh hoạ; .env không đẩy lên GitHub)
DATABASE_URL="postgresql://user:matkhau@localhost:5432/shop"
# Nhắc: thêm .env vào .gitignore để nó KHÔNG bị đẩy lên GitHub Code chỉ đọc giá trị qua tên DATABASE_URL, không biết mật khẩu là gì. Khi deploy lên VPS, mèo con đặt biến môi trường ở máy chủ - cùng một cách giấu bí mật.
- ▸Chuỗi kết nối chứa địa chỉ + user + mật khẩu database - là bí mật.
- ▸Để trong .env, code đọc qua tên biến; KHÔNG hardcode vào mã nguồn.
- ▸.env phải nằm trong .gitignore để không lọt lên GitHub.
Có file schema và chuỗi kết nối rồi, nhưng database vẫn trống - schema mới chỉ là bản vẽ bằng TypeScript. Cần hai bước để biến bản vẽ thành bảng thật trong PostgreSQL, dùng công cụ drizzle-kit đi kèm Drizzle:
Hai lệnh đưa schema vào database (Claude chạy hộ cũng được)
# 1) generate: đọc db/schema.ts, sinh file migration (các câu SQL thay đổi)
npx drizzle-kit generate
# 2) migrate: chạy migration lên database (dùng DATABASE_URL trong .env)
npx drizzle-kit migrate - ▸generate: đọc schema → sinh file migration (lịch sử thay đổi, lưu trong repo).
- ▸migrate: chạy migration lên database để tạo/đổi bảng.
- ▸Migration commit cùng code, nên deploy lên VPS hay ai checkout về cũng dựng lại đúng bảng.
Quan trọng nhất: sau này cần đổi bảng - thêm cột, thêm bảng mới, đổi kiểu dữ liệu - quy trình lặp lại y hệt. Sửa db/schema.ts rồi chạy lại generate và migrate. Drizzle tự so schema mới với hiện trạng và chỉ sinh phần thay đổi:
Trung thực
Khi sản phẩm bắt đầu giữ dữ liệu khách (email, đơn hàng, số dư), mèo con nhận một trách nhiệm: chỉ thu cái thực sự cần, không lưu thừa, và giữ bí mật cẩn thận. Mật khẩu chẳng hạn không bao giờ lưu nguyên văn - phải băm; chuyện này để bài đăng nhập lo.
Bước tiếp theo
Câu hỏi thường gặp
SQL là ngôn ngữ để hỏi và ghi dữ liệu trong database quan hệ. Claude viết hộ các câu SQL; mèo con chỉ cần hiểu khái niệm bảng và quan hệ để mô tả đúng và kiểm lại kết quả. Không cần thuộc cú pháp.
Chia theo loại rồi nối qua id giúp tránh lặp dữ liệu và dễ cập nhật. Ví dụ email khách chỉ nằm một chỗ ở bảng nguoi_dung; đổi một lần là mọi đơn hàng của khách đó vẫn trỏ đúng, không phải sửa hàng trăm chỗ.
ORM (Object-Relational Mapping) là lớp trung gian: mèo con mô tả bảng bằng TypeScript, ORM lo sinh câu SQL và kiểm kiểu dữ liệu giúp. Khoá chọn Drizzle ORM vì nó nhẹ, cú pháp gần SQL nên dễ đọc, và type-safe - gõ sai tên cột là báo lỗi ngay khi code chứ không đợi tới lúc chạy. Hợp với Next.js + TypeScript của khoá.
Không cần thuộc cú pháp SQL, nhưng vẫn nên nắm KHÁI NIỆM bảng, cột và quan hệ (khoá ngoại) - vì Drizzle chỉ là cách diễn đạt tiện hơn cho cùng những thứ đó. Hiểu khái niệm thì mèo con mô tả đúng cho Claude và đọc lại schema Drizzle thấy có hợp lý không.
generate đọc schema (db/schema.ts) và SINH ra file migration - các câu SQL mô tả thay đổi, được lưu trong repo. migrate thì CHẠY các migration đó lên database để thật sự tạo/đổi bảng. Mỗi lần sửa bảng sau này, mèo con lặp lại đúng hai bước: generate rồi migrate.
Vì migration là lịch sử thay đổi của database. Giữ trong repo (commit cùng code) để ai checkout về - hoặc khi deploy lên VPS - cũng dựng lại đúng cấu trúc bảng bằng cách chạy migrate, không phải đoán hay làm tay.
Là một file giữ các giá trị nhạy cảm hoặc hay đổi - như chuỗi kết nối database. Code đọc giá trị từ đó thay vì ghi thẳng vào file mã nguồn. File .env KHÔNG được đẩy lên GitHub, nên bí mật không lọt ra ngoài.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Vì sao web bán hàng cần database (như PostgreSQL)?
- 1
Liệt kê các bảng
Viết ra 4-5 bảng web bán hàng của mèo con cần, mỗi bảng kèm vài cột.
✅ Hoàn thành khi: Có danh sách bảng hợp lý (vd nguoi_dung, san_pham, vi, don_hang) với vài cột cho mỗi bảng.
- 2
Vẽ quan hệ
Chỉ ra bảng nào nối bảng nào qua cột _id (khoá ngoại), dựa theo sơ đồ trong bài.
✅ Hoàn thành khi: Nêu được ít nhất 2 quan hệ, ví dụ vi.nguoi_dung_id → nguoi_dung, don_hang.san_pham_id → san_pham.
- 3
Tạo schema rồi generate & migrate
Nhờ Claude tạo file schema Drizzle (db/schema.ts) cho bảng san_pham, rồi chạy generate và migrate (drizzle-kit) để tạo bảng trong PostgreSQL.
✅ Hoàn thành khi: Có file db/schema.ts + một file migration trong repo, và bảng san_pham tồn tại trong database sau khi migrate.
- 4
Sửa bảng rồi lặp lại y hệt
Thêm một cột mới vào bảng san_pham trong db/schema.ts (vd mo_ta TEXT), rồi chạy lại generate và migrate đúng như lần đầu.
✅ Hoàn thành khi: Bảng san_pham có thêm cột mới sau khi migrate; mèo con nhận ra quy trình sửa schema → generate → migrate lặp lại y hệt.
- 5
Lưu & đọc thử
Nhờ Claude thêm vài sản phẩm vào database rồi hiển thị chúng ở trang /san-pham.
✅ Hoàn thành khi: Mở localhost:3000/san-pham thấy danh sách sản phẩm lấy TỪ database; tải lại trang vẫn còn.
- 6
Giấu chuỗi kết nối vào env
Kiểm tra chuỗi kết nối database nằm trong file .env, và .env có trong .gitignore.
✅ Hoàn thành khi: DATABASE_URL nằm trong .env (không nằm trong file mã nguồn), và .env không bị đẩy lên GitHub.
- 7
Trách nhiệm với dữ liệu
Liệt kê dữ liệu cá nhân web của mèo con định thu, kèm một câu về cách giữ an toàn.
✅ Hoàn thành khi: Có danh sách (vd email, tên) + một câu cam kết (chỉ thu cái cần, không lưu thừa, mật khẩu phải băm).