Thiết kế Prisma Schema: Quan hệ dữ liệu, Enum và Index tối ưu cho khả năng mở rộng
Schema của Prisma không chỉ là file cấu hình ORM đơn thuần, mà đó chính là kiến trúc dữ liệu của ứng dụng. Những quyết định thiết kế sai lầm ở giai đoạn đầu sẽ gây ra hậu quả nghiêm trọng khi ứng dụng phát triển. Bài viết này sẽ đi sâu vào các mẫu quan hệ, cách sử dụng Enum hiệu quả, chiến lược xây dựng Index và các phương pháp tốt nhất để quản lý Migration.

Thiết kế Schema (lược đồ) chính là kiến trúc hệ thống. Schema của Prisma không chỉ là những dòng cấu hình cho ORM, mà nó thể hiện kiến trúc dữ liệu nền tảng của ứng dụng bạn. Những quyết định sai lầm tại bước này sẽ tích tụ và trở nên nghiêm trọng hơn khi ứng dụng phát triển quy mô. Dưới đây là những hướng dẫn quan trọng để xây dựng một Schema Prisma vững chắc và hiệu quả.
Các mẫu quan hệ cốt lõi
Một-nhiều (One-to-Many)
Đây là mối quan hệ phổ biến nhất, ví dụ như một người dùng có nhiều bài viết.
model User {
id String @id @default(cuid())
email String @unique
posts Post[] // Một người dùng có nhiều bài viết
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([userId]) // Luôn luôn index các khóa ngoại
}
Lưu ý quan trọng là bạn luôn nên tạo index cho các trường khóa ngoại (foreign key) để tối ưu hiệu quả truy vấn.
Nhiều-nhiều (Many-to-Many - ẩn định)
Khi bạn không cần thêm thông tin (metadata) vào bảng liên kết, hãy để Prisma tự xử lý.
model Post {
id String @id @default(cuid())
tags Tag[]
}
model Tag {
id String @id @default(cuid())
name String @unique
posts Post[]
}
// Prisma sẽ tự động tạo bảng join (liên kết)
Nhiều-nhiều (Many-to-Many - tường minh)
Khi bạn cần lưu trữ thêm dữ liệu cho mối quan hệ (ví dụ: vai trò trong tổ chức), bạn cần định nghĩa rõ ràng bảng trung gian.
model User {
id String @id @default(cuid())
memberships Membership[]
}
model Organization {
id String @id @default(cuid())
memberships Membership[]
}
model Membership {
id String @id @default(cuid())
userId String
orgId String
role Role @default(MEMBER)
joinedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
org Organization @relation(fields: [orgId], references: [id])
@@unique([userId, orgId]) // Mỗi người dùng chỉ có một membership cho mỗi tổ chức
@@index([orgId])
}
enum Role {
OWNER
ADMIN
MEMBER
}
Sử dụng Enums hiệu quả
Enums (Kiểu liệt kê) giúp dữ liệu chặt chẽ hơn và được kiểm tra ngay ở cấp độ cơ sở dữ liệu.
enum SubscriptionStatus {
TRIALING
ACTIVE
PAST_DUE
CANCELED
PAUSED
}
enum PlanType {
FREE
PRO
ENTERPRISE
}
model Subscription {
id String @id @default(cuid())
userId String @unique
status SubscriptionStatus @default(TRIALING)
plan PlanType @default(FREE)
user User @relation(fields: [userId], references: [id])
}
Lưu ý: Enums được xác thực ở mức cơ sở dữ liệu, không chỉ ở mức ứng dụng. Bạn nên ưu tiên sử dụng Enums thay vì các trường String cho các tập hợp giá trị cố định.
Chỉ mục (Index) thực sự cần thiết
Index đóng vai trò then chốt trong việc duy trì hiệu suất truy vấn.
model Event {
id String @id @default(cuid())
userId String
type String
createdAt DateTime @default(now())
@@index([userId]) // Dùng cho feed sự kiện của user
@@index([type, createdAt]) // Dùng cho lọc theo loại + thời gian
@@index([userId, createdAt]) // Dùng cho hoạt động user sắp xếp theo thời gian
}
Luôn tạo Index cho:
- Các trường là khóa ngoại (Foreign keys).
- Các trường thường xuyên xuất hiện trong mệnh đề
WHERE. - Các trường dùng trong
ORDER BYtrên các bảng dữ liệu lớn. - Các trường có ràng buộc duy nhất (Prisma tự động việc này).
Index phức hợp: Thứ tự các trường rất quan trọng. Hãy đặt trường có tính chọn lọc cao nhất lên đầu, hoặc trường dùng để so sánh bằng (=) trước trường dùng để so sánh khoảng (>, <).
Trường JSON cho dữ liệu linh hoạt
Đối với các dữ liệu không có cấu trúc cố định hoặc thường xuyên thay đổi, loại dữ liệu Json là lựa chọn lý tưởng.
model AuditLog {
id String @id @default(cuid())
userId String
action String
metadata Json // Schema linh hoạt cho dữ liệu đặc thù theo action
createdAt DateTime @default(now())
@@index([userId, createdAt])
@@index([action])
}
Bạn có thể truy cập dữ liệu JSON này một cách an toàn về kiểu (type-safe) trong TypeScript:
// Truy cập JSON an toàn về kiểu dữ liệu
const log = await prisma.auditLog.findFirst();
const meta = log.metadata as { ip: string; userAgent: string };
Xóa mềm (Soft Deletes)
Thay vì xóa vĩnh viễn dữ liệu, hãy dùng kỹ thuật "xóa mềm" để đánh dấu bản ghi là đã bị xóa.
model Post {
id String @id @default(cuid())
title String
deletedAt DateTime? // null = đang hoạt động, có timestamp = đã xóa
@@index([deletedAt]) // Lọc các bản ghi đang hoạt động hiệu quả
}
// Chỉ lấy các bài viết chưa bị xóa
const posts = await prisma.post.findMany({
where: { deletedAt: null },
});
// Thực hiện xóa mềm
await prisma.post.update({
where: { id },
data: { deletedAt: new Date() },
});
Mẫu đa thuê (Multi-Tenancy Pattern)
Đối với các ứng dụng SaaS phục vụ nhiều tổ chức, việc thiết kế schema hỗ trợ đa thuê (multi-tenancy) là bắt buộc.
model Organization {
id String @id @default(cuid())
name String
slug String @unique
users User[]
projects Project[]
}
model Project {
id String @id @default(cuid())
name String
orgId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
@@index([orgId])
@@unique([orgId, name]) // Tên dự án phải duy nhất trong một tổ chức
}
Mọi truy vấn trong phạm vi thuê đều phải bao gồm orgId trong mệnh đề where. Index giúp đảm bảo các truy vấn này nhanh chóng ngay cả khi bảng có hàng triệu dòng dữ liệu.
Các phương pháp tốt nhất cho Migration
# Phát triển: tạo và áp dụng migration
npx prisma migrate dev --name add-subscription-table
# Sản xuất: áp dụng các migration đang chờ
npx prisma migrate deploy
# KHÔNG BAO GIỜ chỉnh sửa migration đã có
# Hãy tạo migration mới để sửa lỗi
Các migration nguy hiểm cần xem xét kỹ:
- Thêm cột
NOT NULLvào bảng hiện có (cần giá trị mặc định hoặc điền dữ liệu trước). - Xóa cột (mất dữ liệu, cần cập nhật code ứng dụng trước).
- Thay đổi kiểu cột (có thể cần chuyển đổi kiểu rõ ràng).
Đối với các bảng lớn, hãy sử dụng tùy chọn CONCURRENTLY để tạo index trực tiếp trong SQL nhằm tránh khóa bảng:
-- Trong file migration
CREATE INDEX CONCURRENTLY "Event_userId_createdAt_idx" ON "Event"("userId", "createdAt");
Schema của bạn là một hợp đồng với cơ sở dữ liệu. Việc thay đổi dễ dàng và rẻ ở giai đoạn đầu, nhưng sẽ rất tốn kém ở giai đoạn sau. Hãy suy nghĩ kỹ về các mối quan hệ và index trước khi bạn bắt đầu nhập dữ liệu thực tế.
Bài viết liên quan

Phần mềm
Anthropic ra mắt Claude Opus 4.7: Nâng cấp mạnh mẽ cho lập trình nhưng vẫn thua Mythos Preview
16 tháng 4, 2026

Công nghệ
Qwen3.6-35B-A3B: Quyền năng Lập trình Agentic, Nay Đã Mở Cửa Cho Tất Cả
16 tháng 4, 2026

Công nghệ
Spotify thắng kiện 322 triệu USD từ nhóm pirate Anna's Archive nhưng đối mặt với bài toán thu hồi
16 tháng 4, 2026
