•9 min read

Các chiến lược Database Seeding với Prisma và TypeScript

Các chiến lược Database Seeding với Prisma và TypeScript

Việc tạo dữ liệu ban đầu cho cơ sở dữ liệu (database seeding) thường bị xem nhẹ—chỉ là một đoạn script viết vội để đổ vài dòng dữ liệu giả vào môi trường phát triển. Tuy nhiên, khi ứng dụng mở rộng và mô hình miền (domain model) ngày càng phức tạp, một script seed dễ vỡ sẽ nhanh chóng trở thành nút thắt cổ chai đáng kể đối với năng suất của nhà phát triển và kiểm thử tự động.

Trong hệ sinh thái Node.js hiện đại, việc kết hợp Prisma và TypeScript mang lại một nền tảng cực kỳ mạnh mẽ để xây dựng các công cụ tạo dữ liệu ban đầu cho cơ sở dữ liệu mạnh mẽ, an toàn về kiểu (type-safe). Trong hướng dẫn này, chúng ta sẽ khám phá các chiến lược nâng cao cho việc tạo dữ liệu ban đầu, vượt xa các hướng dẫn thông thường. Chúng ta sẽ tập trung vào việc tạo dữ liệu theo chương trình (programmatic seeding), tích hợp Faker.js để có dữ liệu giả lập thực tế, và quan trọng nhất là cách xử lý dữ liệu quan hệ lồng sâu một cách tinh tế.

Audio Briefing
0:00 / 0:00

Nền tảng: Tại sao cách tạo dữ liệu cơ bản thất bại

Hướng dẫn Prisma cơ bản điển hình đề xuất viết một script đơn giản thực thi một loạt các lệnh prisma.user.create(). Mặc dù cách này hiệu quả trong ngày đầu tiên, nhưng nó sẽ sụp đổ vào ngày thứ mười. Các script cơ bản mắc phải một số lỗi nghiêm trọng:

  1. Thiếu tính bất biến (Idempotency): Chạy script hai lần sẽ dẫn đến vi phạm ràng buộc duy nhất (unique constraint).
  2. Dữ liệu cứng (Hardcoded Data): Dữ liệu được định nghĩa tĩnh giới hạn khả năng kiểm thử phân trang, tính năng tìm kiếm hoặc các trạng thái UI phức tạp.
  3. Mớ hỗn độn quan hệ (Relational Spaghetti): Quản lý khóa ngoại (foreign key) thủ công trên hàng chục bảng rất dễ xảy ra lỗi.
  4. Vấn đề hiệu suất: Việc chờ đợi hàng ngàn thao tác chèn riêng lẻ một cách tuần tự sẽ làm chậm đáng kể các pipeline CI.

Để xây dựng một chiến lược tạo dữ liệu ban đầu chuyên nghiệp, chúng ta cần giải quyết từng thách thức này một cách có hệ thống.

Advertisement

Tính bất biến và làm sạch cơ sở dữ liệu

Một script seed phải có tính bất biến. Bạn có thể chạy nó nhiều lần mà không làm hỏng trạng thái cơ sở dữ liệu hoặc gặp lỗi. Nhìn chung có hai cách tiếp cận để đạt được điều này: làm sạch triệt để hoặc upsert cẩn thận.

Chiến lược 1: Làm sạch triệt để

Nếu bạn muốn có một trạng thái hoàn toàn mới mỗi khi bạn seed, bạn cần xóa sạch cơ sở dữ liệu. Với Prisma, việc xóa các bản ghi theo đúng thứ tự để tuân thủ các ràng buộc khóa ngoại có thể rất tẻ nhạt. Thay vì xóa thủ công từ mỗi bảng, bạn có thể cắt bớt (truncate) các bảng một cách linh hoạt.

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function cleanDatabase() {
  const tableNames = await prisma.$queryRaw<
    Array<{ tablename: string }>
  >`SELECT tablename FROM pg_tables WHERE schemaname='public'`;

  const tables = tableNames
    .map(({ tablename }) => tablename)
    .filter((name) => name !== '_prisma_migrations')
    .map((name) => `"public"."${name}"`)
    .join(', ');

  try {
    await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tables} CASCADE;`);
    console.log('Database cleaned successfully.');
  } catch (error) {
    console.error('Error cleaning database', error);
  }
}

Lưu ý: Đoạn mã trên sử dụng cú pháp PostgreSQL. Cách tiếp cận sẽ khác nhau tùy thuộc vào công cụ cơ sở dữ liệu cơ bản của bạn.

Chiến lược 2: Upsert cho dữ liệu cốt lõi

Đối với các cấu hình cốt lõi, vai trò hoặc phân loại phải tồn tại, upsert là người bạn tốt nhất của bạn. upsert đảm bảo rằng một bản ghi tồn tại mà không gây ra lỗi nếu nó đã có sẵn.

async function seedRoles() {
  const roles = ['ADMIN', 'USER', 'EDITOR'];

  for (const roleName of roles) {
    await prisma.role.upsert({
      where: { name: roleName },
      update: {},
      create: { name: roleName },
    });
  }
}

Bằng cách kết hợp cắt bớt cho dữ liệu giả lập và upsert cho dữ liệu nền tảng, bạn tạo ra một nền tảng tạo dữ liệu ban đầu mạnh mẽ, có thể lặp lại.

Tích hợp Faker.js cho dữ liệu thực tế

Để mô phỏng việc sử dụng trong thế giới thực, chúng ta cần một lượng lớn dữ liệu thực tế. @faker-js/faker là tiêu chuẩn công nghiệp cho việc này.

Khi tích hợp Faker vào các script seed của bạn, bước quan trọng nhất (và thường bị bỏ qua) là thiết lập một seed xác định. Điều này đảm bảo rằng mọi nhà phát triển trong nhóm của bạn và môi trường CI của bạn đều tạo ra cùng một dữ liệu "ngẫu nhiên" chính xác.

import { faker } from '@faker-js/faker';

// Set a deterministic seed for consistent generation
faker.seed(12345);

function createUserFactory() {
  const firstName = faker.person.firstName();
  const lastName = faker.person.lastName();
  
  return {
    email: faker.internet.email({ firstName, lastName }).toLowerCase(),
    name: `${firstName} ${lastName}`,
    bio: faker.lorem.paragraph(),
    avatarUrl: faker.image.avatar(),
  };
}

Sử dụng các hàm factory giúp logic seeding chính của bạn gọn gàng và dễ dàng tạo ra các mảng dữ liệu.

Nắm vững dữ liệu quan hệ

Sự phức tạp thực sự của việc tạo dữ liệu ban đầu nằm ở việc quản lý các mối quan hệ. Các thao tác ghi lồng nhau của Prisma cung cấp một giải pháp thanh lịch để tạo các biểu đồ dữ liệu liên quan trong một giao dịch duy nhất.

Ghi sâu (Deep Writes)

Thay vì tạo một User, lấy ID của họ, rồi tạo các Post được ánh xạ tới ID đó, bạn có thể làm tất cả cùng một lúc:

async function seedUserWithPosts() {
  await prisma.user.create({
    data: {
      ...createUserFactory(),
      posts: {
        create: Array.from({ length: 5 }).map(() => ({
          title: faker.lorem.sentence(),
          content: faker.lorem.paragraphs(3),
          published: faker.datatype.boolean(),
        })),
      },
    },
  });
}

Cách tiếp cận này rất dễ đọc và đảm bảo tính toàn vẹn tham chiếu mà không cần theo dõi ID thủ công.

Giải quyết các mối quan hệ phức tạp

Điều gì sẽ xảy ra nếu bạn có các mối quan hệ nhiều-nhiều (many-to-many), hoặc các mối quan hệ với các bản ghi được tạo động? Ví dụ, gán các Tag ngẫu nhiên cho các Post.

Để xử lý điều này, bạn cần tạo trước một nhóm các thực thể liên quan, sau đó chọn ngẫu nhiên từ chúng trong giai đoạn tạo.

async function seedComplexGraph() {
  // 1. Create a pool of tags
  const tagsData = Array.from({ length: 10 }).map(() => ({
    name: faker.word.noun(),
  }));
  
  await prisma.tag.createMany({ data: tagsData });
  const allTags = await prisma.tag.findMany();

  // 2. Helper to get random tags
  const getRandomTags = (count: number) => {
    return faker.helpers.arrayElements(allTags, count).map(tag => ({
      id: tag.id
    }));
  };

  // 3. Create posts and connect random tags
  await prisma.post.create({
    data: {
      title: faker.lorem.sentence(),
      content: faker.lorem.paragraphs(),
      tags: {
        connect: getRandomTags(3),
      },
      author: {
        create: createUserFactory(),
      }
    }
  });
}

Sử dụng cú pháp connect cho phép bạn liên kết các bản ghi mới được tạo với các bản ghi hiện có một cách dễ dàng.

Advertisement

Hiệu suất: Xử lý theo lô và giao dịch

Khi mở rộng script seed của bạn để tạo hàng chục nghìn bản ghi, việc chờ đợi từng lệnh prisma.model.create() riêng lẻ sẽ dẫn đến suy giảm hiệu suất nghiêm trọng do chi phí mạng và các chuyến đi khứ hồi đến cơ sở dữ liệu.

Để tối ưu hóa, hãy sử dụng createMany kết hợp với phân đoạn (chunking).

async function seedLargeVolumeOfUsers() {
  const totalUsers = 10000;
  const batchSize = 1000;
  
  for (let i = 0; i < totalUsers; i += batchSize) {
    const usersBatch = Array.from({ length: batchSize }).map(createUserFactory);
    
    await prisma.user.createMany({
      data: usersBatch,
      skipDuplicates: true,
    });
    
    console.log(`Seeded batch ${i / batchSize + 1}`);
  }
}

createMany thực thi một câu lệnh INSERT duy nhất cho toàn bộ mảng, tăng tốc quá trình lên nhiều bậc.

Nếu bạn có dữ liệu quan hệ phức tạp không thể sử dụng createMany (vì createMany không hỗ trợ các mối quan hệ lồng nhau), bạn có thể quay lại các giao dịch Prisma (prisma.$transaction) để nhóm nhiều lệnh create vào một giao dịch cơ sở dữ liệu duy nhất, giảm đáng kể chi phí commit.

Cấu trúc thư mục Seed

Khi logic seed của bạn phát triển, một tệp seed.ts duy nhất sẽ trở nên khó quản lý. Hãy áp dụng mô hình factory và runner:

prisma/
  seed/
    index.ts        # The main entry point orchestrator
    factories/
      user.ts       # User factory functions
      post.ts       # Post factory functions
    runners/
      roles.ts      # Logic to seed foundational roles
      mockData.ts   # Logic to orchestrate Faker data

Tệp index.ts của bạn chỉ đơn giản là điều phối các runner:

import { PrismaClient } from '@prisma/client';
import { seedRoles } from './runners/roles';
import { seedMockData } from './runners/mockData';
import { cleanDatabase } from './utils/clean';

const prisma = new PrismaClient();

async function main() {
  console.log('Starting seed process...');
  
  if (process.env.NODE_ENV !== 'production') {
    await cleanDatabase();
  }
  
  await seedRoles(prisma);
  
  if (process.env.NODE_ENV === 'development') {
    await seedMockData(prisma);
  }
  
  console.log('Seed completed.');
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

Kết luận

Tạo dữ liệu ban đầu không chỉ là một công việc vặt; nó là một thành phần quan trọng của một văn hóa kỹ thuật mạnh mẽ. Bằng cách tận dụng tính an toàn kiểu của TypeScript, cú pháp ORM biểu cảm của Prisma và Faker.js, bạn có thể xây dựng cơ sở hạ tầng tạo dữ liệu ban đầu giúp nhóm của bạn phát triển thay vì kìm hãm họ.

Hãy áp dụng tính bất biến, sử dụng các thao tác ghi lồng nhau cho các mối quan hệ và tối ưu hóa bằng cách xử lý theo lô. Bạn của tương lai—và nhóm QA của bạn—sẽ cảm ơn bạn.

Bạn cũng có thể thích

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement