Thiết kế Prisma Schema: Quan hệ dữ liệu, Enum và Index tối ưu cho khả năng mở rộng

07 tháng 4, 2026·6 phút đọc

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ế Prisma Schema: Quan hệ dữ liệu, Enum và Index tối ưu cho khả năng mở rộng

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 BY trê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 NULL và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 được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗