PlanetScale 与 Prisma 集成 #

本章将介绍如何在 Prisma ORM 中集成 PlanetScale 数据库,利用 Prisma 的强大功能简化数据库操作。

Prisma 简介 #

为什么选择 Prisma? #

text
┌─────────────────────────────────────────────────────────────┐
│                    Prisma 优势                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ✅ 类型安全                                               │
│      - 自动生成 TypeScript 类型                             │
│      - 编译时错误检查                                       │
│                                                             │
│   ✅ 直观的 API                                             │
│      - 链式调用                                             │
│      - 自动补全                                             │
│                                                             │
│   ✅ 数据库无关                                             │
│      - 支持 MySQL、PostgreSQL、SQLite 等                   │
│      - 统一的 API                                          │
│                                                             │
│   ✅ 强大的工具                                             │
│      - Prisma Studio(可视化管理)                         │
│      - Prisma Migrate(迁移管理)                          │
│      - Prisma Client(类型安全客户端)                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

项目配置 #

安装 Prisma #

bash
# 安装 Prisma
npm install prisma @prisma/client

# 初始化 Prisma
npx prisma init

配置 schema.prisma #

prisma
datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

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

  @@index([authorId])
}

环境变量 #

env
DATABASE_URL="mysql://abc123:pscale_pw_xxx@aws.connect.psdb.cloud/my-database?sslaccept=strict"

重要配置说明 #

relationMode 设置 #

text
┌─────────────────────────────────────────────────────────────┐
│                    relationMode 说明                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   PlanetScale 不支持外键约束,需要设置:                    │
│                                                             │
│   datasource db {                                          │
│     provider     = "mysql"                                 │
│     url          = env("DATABASE_URL")                     │
│     relationMode = "prisma"                                │
│   }                                                        │
│                                                             │
│   relationMode = "prisma" 的作用:                         │
│   - Prisma 在应用层模拟外键关系                            │
│   - 不依赖数据库外键约束                                   │
│   - 兼容 PlanetScale 的架构                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

迁移策略 #

text
┌─────────────────────────────────────────────────────────────┐
│                    迁移策略                                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   PlanetScale 使用分支管理 Schema,因此:                   │
│                                                             │
│   ❌ 不要使用 prisma migrate                               │
│      - prisma migrate 需要外键支持                         │
│      - 与 PlanetScale 分支工作流冲突                       │
│                                                             │
│   ✅ 使用 prisma db push                                   │
│      - 直接推送 Schema 变更                                │
│      - 不创建迁移文件                                      │
│      - 配合 PlanetScale 分支使用                           │
│                                                             │
│   推荐工作流:                                              │
│   1. 创建 PlanetScale 开发分支                             │
│   2. 修改 schema.prisma                                    │
│   3. 运行 prisma db push                                   │
│   4. 创建 Deploy Request                                   │
│   5. 部署到生产                                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

CRUD 操作 #

创建 Prisma Client #

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

const prisma = new PrismaClient();

export default prisma;

查询操作 #

typescript
import prisma from './prisma';

// 查询所有用户
const users = await prisma.user.findMany();

// 条件查询
const user = await prisma.user.findFirst({
  where: {
    email: 'test@example.com'
  }
});

// 根据 ID 查询
const user = await prisma.user.findUnique({
  where: {
    id: 1
  }
});

// 关联查询
const usersWithPosts = await prisma.user.findMany({
  include: {
    posts: true
  }
});

// 分页查询
const users = await prisma.user.findMany({
  skip: 0,
  take: 10,
  orderBy: {
    createdAt: 'desc'
  }
});

// 聚合查询
const count = await prisma.user.count();

创建操作 #

typescript
import prisma from './prisma';

// 创建单个记录
const user = await prisma.user.create({
  data: {
    email: 'test@example.com',
    name: 'Test User'
  }
});

// 创建并关联
const post = await prisma.post.create({
  data: {
    title: 'My First Post',
    content: 'Hello World',
    author: {
      connect: {
        id: 1
      }
    }
  }
});

// 批量创建
const users = await prisma.user.createMany({
  data: [
    { email: 'user1@example.com', name: 'User 1' },
    { email: 'user2@example.com', name: 'User 2' }
  ]
});

更新操作 #

typescript
import prisma from './prisma';

// 更新单个记录
const user = await prisma.user.update({
  where: {
    id: 1
  },
  data: {
    name: 'Updated Name'
  }
});

// 条件更新
const result = await prisma.user.updateMany({
  where: {
    email: { contains: '@old-domain.com' }
  },
  data: {
    email: 'new@example.com'
  }
});

// 更新或创建
const user = await prisma.user.upsert({
  where: {
    email: 'test@example.com'
  },
  update: {
    name: 'Updated Name'
  },
  create: {
    email: 'test@example.com',
    name: 'New User'
  }
});

删除操作 #

typescript
import prisma from './prisma';

// 删除单个记录
await prisma.user.delete({
  where: {
    id: 1
  }
});

// 条件删除
await prisma.user.deleteMany({
  where: {
    email: { contains: '@test.com' }
  }
});

Express 集成示例 #

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

const app = express();
app.use(express.json());

const prisma = new PrismaClient();

app.get('/users', async (req, res) => {
  const users = await prisma.user.findMany({
    include: { posts: true }
  });
  res.json(users);
});

app.get('/users/:id', async (req, res) => {
  const user = await prisma.user.findUnique({
    where: { id: parseInt(req.params.id) },
    include: { posts: true }
  });
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  res.json(user);
});

app.post('/users', async (req, res) => {
  const user = await prisma.user.create({
    data: req.body
  });
  res.status(201).json(user);
});

app.put('/users/:id', async (req, res) => {
  const user = await prisma.user.update({
    where: { id: parseInt(req.params.id) },
    data: req.body
  });
  res.json(user);
});

app.delete('/users/:id', async (req, res) => {
  await prisma.user.delete({
    where: { id: parseInt(req.params.id) }
  });
  res.status(204).send();
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Next.js 集成示例 #

API Route #

typescript
import { PrismaClient } from '@prisma/client';
import type { NextApiRequest, NextApiResponse } from 'next';

const prisma = new PrismaClient();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'GET') {
    const users = await prisma.user.findMany();
    return res.status(200).json(users);
  }

  if (req.method === 'POST') {
    const user = await prisma.user.create({
      data: req.body
    });
    return res.status(201).json(user);
  }

  res.status(405).json({ error: 'Method not allowed' });
}

Server Component #

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

const prisma = new PrismaClient();

export default async function UsersPage() {
  const users = await prisma.user.findMany();

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name} ({user.email})</li>
      ))}
    </ul>
  );
}

最佳实践 #

连接管理 #

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

const globalForPrisma = global as unknown as { prisma: PrismaClient };

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

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

错误处理 #

typescript
import { Prisma } from '@prisma/client';

try {
  const user = await prisma.user.create({
    data: { email: 'existing@example.com' }
  });
} catch (error) {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    if (error.code === 'P2002') {
      console.log('Unique constraint violation');
    }
  }
  throw error;
}

下一步 #

现在你已经掌握了 Prisma 集成,接下来学习 Vercel 集成,了解如何部署到 Vercel!

最后更新:2026-03-29