Chuyển đến nội dung chính

第 14 課:資料庫整合與 Prisma

Prisma ORM 設定、模式設計。遷徙、播種。關係(1-1、1-N、M-N)。進階查詢、交易。 Prisma 客戶端擴充。

💻 程式設計 — 第 14 課 第 14 課:資料庫整合與 Prisma

React 和 Next.js:從基礎到高級

第 4 部分:Next.js 高級

亞洲開發網

1. 設定 Prisma

npm install prisma @prisma/client
npx prisma init --datasource-provider postgresql

2. 架構設計

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

model Profile {
  id     String  @id @default(cuid())
  bio    String?
  avatar String?
  userId String  @unique
  user   User    @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Post {
  id         String     @id @default(cuid())
  title      String
  content    String?
  published  Boolean    @default(false)
  authorId   String
  author     User       @relation(fields: [authorId], references: [id])
  categories Category[]
  tags       Tag[]
  createdAt  DateTime   @default(now())
  updatedAt  DateTime   @updatedAt

  @@index([authorId])
  @@index([published, createdAt])
}

model Category {
  id    String @id @default(cuid())
  name  String @unique
  slug  String @unique
  posts Post[]
}

model Tag {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[]
}

enum Role {
  USER
  ADMIN
  EDITOR
}

3. 遷移和播種

# Create migration
npx prisma migrate dev --name init

# Apply in production
npx prisma migrate deploy

# Reset database
npx prisma migrate reset
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

async function main() {
  await prisma.user.create({
    data: {
      email: '[email protected]',
      name: 'Admin',
      role: 'ADMIN',
      profile: { create: { bio: 'Administrator' } },
      posts: {
        create: [
          { title: 'First Post', content: 'Hello!', published: true },
          { title: 'Draft', content: 'WIP' },
        ],
      },
    },
  });
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());

4.Prisma 客戶端單例

// lib/db.ts
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const db =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === 'development' ? ['query'] : [],
  });

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;

5. 進階查詢

// Pagination + Filter + Sort
const posts = await db.post.findMany({
  where: {
    published: true,
    OR: [
      { title: { contains: search, mode: 'insensitive' } },
      { content: { contains: search, mode: 'insensitive' } },
    ],
    categories: { some: { slug: categorySlug } },
  },
  include: {
    author: { select: { name: true, email: true } },
    categories: true,
    _count: { select: { tags: true } },
  },
  orderBy: { createdAt: 'desc' },
  skip: (page - 1) * limit,
  take: limit,
});

// Count
const total = await db.post.count({ where: { published: true } });

// Transaction
const [post, user] = await db.$transaction([
  db.post.create({ data: { title: 'New', authorId: userId } }),
  db.user.update({
    where: { id: userId },
    data: { postCount: { increment: 1 } },
  }),
]);

// Interactive transaction
await db.$transaction(async (tx) => {
  const user = await tx.user.findUnique({ where: { id: userId } });
  if (!user) throw new Error('User not found');
  await tx.post.create({ data: { title: 'Post', authorId: user.id } });
});

6. 在伺服器元件中使用

import { db } from '@/lib/db';
import { Suspense } from 'react';

async function PostList() {
  const posts = await db.post.findMany({
    where: { published: true },
    include: { author: true },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div className="grid gap-4">
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.author.name}</p>
        </article>
      ))}
    </div>
  );
}

export default function PostsPage() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <PostList />
    </Suspense>
  );
}

下一篇: 國際化和搜尋引擎優化 — 多語言、元資料、結構化資料。