Skip to content
On this page

Prisma 最佳实践

本指南总结了在实际项目中使用 Prisma 的最佳实践,涵盖架构设计、性能优化、安全性和维护性等方面。

项目结构最佳实践

目录结构

src/
├── prisma/
│   ├── schema.prisma      # Prisma schema 文件
│   ├── migrations/        # 数据库迁移文件
│   └── seed.ts           # 数据库种子脚本
├── lib/
│   └── prisma.ts         # Prisma 客户端实例
├── models/               # 数据模型相关的服务
│   ├── user-service.ts
│   └── post-service.ts
├── types/
│   └── prisma-types.ts   # 自定义类型定义
└── middleware/
    └── prisma-middleware.ts

Prisma 客户端管理

typescript
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'

declare global {
  var prisma: PrismaClient | undefined
}

const prismaClient = global.prisma || new PrismaClient()

if (process.env.NODE_ENV === 'development') {
  global.prisma = prismaClient
}

export default prismaClient

// 在应用关闭时优雅地断开连接
process.on('beforeExit', async () => {
  await prismaClient.$disconnect()
})

Schema 设计最佳实践

合理的模型设计

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

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

model User {
  id           Int       @id @default(autoincrement())
  email        String    @unique @db.VarChar(255)  // 明确指定长度
  name         String?   @db.VarChar(255)
  passwordHash String    @db.Text                  // 密码哈希可能较长
  role         Role      @default(USER)
  status       UserStatus @default(ACTIVE)
  
  // 时间戳
  createdAt    DateTime  @default(now())
  updatedAt    DateTime  @updatedAt
  
  // 关系
  posts        Post[]
  profile      UserProfile?
  
  // 索引
  @@index([role])
  @@index([status, createdAt])
  @@map("users")  // 明确指定数据库表名
}

model Post {
  id          Int       @id @default(autoincrement())
  title       String    @db.VarChar(255)
  content     String?   @db.Text
  slug        String    @unique                       // URL 友好标识
  published   Boolean   @default(false)
  publishedAt DateTime?                               // 发布时间
  authorId    Int
  author      User      @relation(fields: [authorId], references: [id], onDelete: Cascade)
  tags        Tag[]     @relation(references: [id])
  
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  
  @@index([authorId, published])
  @@index([publishedAt])
  @@fulltext([title, content]) if (provider == "mysql")  // 全文搜索
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[] @relation(references: [id])
  
  @@index([name])
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

enum UserStatus {
  ACTIVE
  INACTIVE
  SUSPENDED
}

关系建模最佳实践

prisma
// 多对多关系的最佳实践
model User {
  id        Int            @id @default(autoincrement())
  email     String         @unique
  projects  UserProject[]  // 使用显式关系表
}

model Project {
  id        Int            @id @default(autoincrement())
  name      String
  users     UserProject[]  // 使用显式关系表
}

// 显式关系表,可包含额外字段
model UserProject {
  id         Int      @id @default(autoincrement())
  userId     Int
  projectId  Int
  role       String   // 关系中的额外信息
  assignedAt DateTime @default(now())
  
  user       User     @relation(fields: [userId], references: [id])
  project    Project  @relation(fields: [projectId], references: [id])
  
  @@unique([userId, projectId])  // 防止重复关系
  @@index([userId])
  @@index([projectId])
}

查询优化最佳实践

选择合适的查询方法

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

const prisma = new PrismaClient()

// 1. 使用 findUnique 而不是 findFirst 来查找唯一记录
async function findUserEfficiently(email: string) {
  // 好的做法:利用唯一索引
  const user = await prisma.user.findUnique({
    where: { email }
  })
  
  // 避免的做法:使用 findFirst 查找唯一字段
  // const user = await prisma.user.findFirst({
  //   where: { email }  // 这不会利用唯一索引的优势
  // })
  
  return user
}

// 2. 合理使用 select 和 include
async function getOptimizedUserData(userId: number) {
  // 好的做法:只选择需要的字段
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: {
      id: true,
      email: true,
      name: true,
      posts: {
        select: {
          id: true,
          title: true,
          published: true,
          _count: {
            select: { comments: true }  // 使用聚合而不是单独查询
          }
        },
        where: { published: true },
        take: 10,
        orderBy: { createdAt: 'desc' }
      }
    }
  })
  
  return user
}

// 3. 避免 N+1 查询
async function getPostsWithAuthorsEfficiently() {
  // 好的做法:单次查询获取所有数据
  const posts = await prisma.post.findMany({
    include: {
      author: {
        select: {
          id: true,
          name: true,
          email: true
        }
      },
      tags: true
    }
  })
  
  return posts
}

分页最佳实践

typescript
// 游标分页实现
interface PaginationArgs {
  first?: number
  after?: string
  last?: number
  before?: string
}

async function getCursorPaginatedUsers(args: PaginationArgs) {
  const first = args.first || 10
  const cursor = args.after ? { id: parseInt(args.after) } : undefined
  
  const users = await prisma.user.findMany({
    take: first,
    ...(cursor && { cursor }),
    orderBy: { id: 'asc' as const },
    include: {
      _count: {
        select: { posts: true }
      }
    }
  })
  
  // 获取下一个游标
  const hasNextPage = users.length === first
  const endCursor = hasNextPage ? users[first - 1].id.toString() : null
  
  return {
    users,
    pageInfo: {
      hasNextPage,
      endCursor
    }
  }
}

// 带计数的分页(适用于需要总数量的场景)
async function getOffsetPaginatedUsers(page: number, limit: number) {
  const skip = (page - 1) * limit
  
  const [users, totalCount] = await Promise.all([
    prisma.user.findMany({
      skip,
      take: limit,
      orderBy: { createdAt: 'desc' }
    }),
    prisma.user.count()
  ])
  
  return {
    users,
    totalCount,
    totalPages: Math.ceil(totalCount / limit),
    currentPage: page
  }
}

事务管理最佳实践

事务使用原则

typescript
// 1. 保持事务简短
async function transferFunds(fromId: number, toId: number, amount: number) {
  // 在事务中只执行必要的数据库操作
  const result = await prisma.$transaction(async (tx) => {
    // 检查余额
    const fromAccount = await tx.account.findUnique({
      where: { id: fromId }
    })
    
    if (!fromAccount || fromAccount.balance < amount) {
      throw new Error('Insufficient funds')
    }
    
    // 执行转账
    await Promise.all([
      tx.account.update({
        where: { id: fromId },
        data: { balance: { decrement: amount } }
      }),
      tx.account.update({
        where: { id: toId },
        data: { balance: { increment: amount } }
      })
    ])
    
    // 记录交易
    const transaction = await tx.transaction.create({
      data: {
        fromAccountId: fromId,
        toAccountId: toId,
        amount,
        type: 'TRANSFER'
      }
    })
    
    return transaction
  })
  
  return result
}

// 2. 事务中避免外部 API 调用
async function processOrderSafely(orderData: any) {
  // 不好的做法:在事务中调用外部 API
  // const result = await prisma.$transaction(async (tx) => {
  //   const order = await tx.order.create({ data: orderData })
  //   const paymentResult = await externalPaymentService.charge(order.total)  // 外部调用
  //   return order
  // })
  
  // 好的做法:先处理外部依赖,再执行数据库事务
  const paymentResult = await externalPaymentService.charge(orderData.total)
  
  if (!paymentResult.success) {
    throw new Error('Payment failed')
  }
  
  const order = await prisma.$transaction(async (tx) => {
    return await tx.order.create({
      data: {
        ...orderData,
        paymentStatus: 'PAID',
        transactionId: paymentResult.transactionId
      }
    })
  })
  
  return order
}

安全最佳实践

输入验证和清理

typescript
import { z } from 'zod'

// 使用 Zod 进行输入验证
const userSchema = z.object({
  email: z.string().email('Invalid email format'),
  name: z.string().min(2).max(100),
  age: z.number().min(0).max(150).optional()
})

class UserService {
  async createUser(userData: unknown) {
    // 验证输入
    const validatedData = userSchema.parse(userData)
    
    // 创建用户
    const user = await prisma.user.create({
      data: {
        email: validatedData.email.toLowerCase(), // 标准化输入
        name: validatedData.name.trim(),
        age: validatedData.age
      }
    })
    
    return user
  }
}

// 防止过度获取数据
async function secureUserUpdate(userId: number, updateData: any) {
  // 只允许更新特定字段
  const allowedFields = ['name', 'email', 'avatar']
  const filteredData: any = {}
  
  for (const [key, value] of Object.entries(updateData)) {
    if (allowedFields.includes(key)) {
      filteredData[key] = value
    }
  }
  
  const user = await prisma.user.update({
    where: { id: userId },
    data: filteredData
  })
  
  return user
}

权限控制

typescript
// 基于角色的访问控制
interface UserSession {
  id: number
  role: string
  permissions: string[]
}

class AuthorizationService {
  static hasPermission(session: UserSession, resource: string, action: string): boolean {
    const permission = `${resource}:${action}`
    return session.permissions.includes(permission)
  }
  
  static async canAccessUser(session: UserSession, targetUserId: number): Promise<boolean> {
    if (session.id === targetUserId) return true  // 用户可以访问自己
    if (session.role === 'ADMIN') return true     // 管理员可以访问任何用户
    return false
  }
}

async function getSecureUser(session: UserSession, userId: number) {
  const canAccess = await AuthorizationService.canAccessUser(session, userId)
  
  if (!canAccess) {
    throw new Error('Access denied')
  }
  
  // 只返回非敏感信息
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: {
      id: true,
      name: true,
      email: true,
      createdAt: true,
      // 不返回密码、角色等敏感信息
    }
  })
  
  return user
}

错误处理最佳实践

统一错误处理

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

interface ServiceResponse<T> {
  success: boolean
  data?: T
  error?: string
  code?: string
}

class PrismaService {
  static async handlePrismaError<T>(
    operation: () => Promise<T>
  ): Promise<ServiceResponse<T>> {
    try {
      const data = await operation()
      return { success: true, data }
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError) {
        // 处理已知的 Prisma 错误
        switch (error.code) {
          case 'P2002': // Unique constraint failed
            return {
              success: false,
              error: 'Resource already exists',
              code: 'UNIQUE_CONSTRAINT'
            }
          case 'P2025': // Record not found
            return {
              success: false,
              error: 'Resource not found',
              code: 'NOT_FOUND'
            }
          default:
            return {
              success: false,
              error: 'Database operation failed',
              code: 'DATABASE_ERROR'
            }
        }
      }
      
      // 处理其他错误
      return {
        success: false,
        error: 'An unexpected error occurred',
        code: 'UNKNOWN_ERROR'
      }
    }
  }
}

// 使用统一错误处理
async function createUserSafely(userData: any) {
  const result = await PrismaService.handlePrismaError(() =>
    prisma.user.create({ data: userData })
  )
  
  return result
}

性能监控最佳实践

查询性能监控

typescript
// 性能监控装饰器
function MonitorPerformance(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const method = descriptor.value
  
  descriptor.value = async function (...args: any[]) {
    const start = performance.now()
    const result = await method.apply(this, args)
    const end = performance.now()
    
    console.log(`${propertyName} took ${end - start} milliseconds`)
    
    if (end - start > 1000) { // 超过1秒的查询
      console.warn(`Slow query detected in ${propertyName}`)
    }
    
    return result
  }
}

class OptimizedUserService {
  @MonitorPerformance
  async getUsersWithPosts() {
    return await prisma.user.findMany({
      include: {
        posts: {
          where: { published: true },
          select: {
            id: true,
            title: true,
            _count: {
              select: { comments: true }
            }
          }
        }
      }
    })
  }
}

// 中间件监控
const performanceMiddleware: Prisma.Middleware = async (params, next) => {
  const start = performance.now()
  const result = await next(params)
  const end = performance.now()
  
  const duration = end - start
  
  if (duration > 100) { // 记录超过100ms的查询
    console.log(`Slow query: ${params.model}.${params.action} took ${duration}ms`)
  }
  
  return result
}

prisma.$use(performanceMiddleware)

测试最佳实践

测试数据管理

typescript
// 测试数据工厂
class TestDataFactory {
  static async createUser(overrides: Partial<any> = {}) {
    return await prisma.user.create({
      data: {
        email: `test${Date.now()}${Math.random()}@example.com`,
        name: 'Test User',
        ...overrides
      }
    })
  }
  
  static async createPost(overrides: Partial<any> = {}) {
    const user = overrides.userId ? 
      { connect: { id: overrides.userId } } : 
      { create: { email: `post-user${Date.now()}@example.com`, name: 'Post Creator' } }
    
    return await prisma.post.create({
      data: {
        title: 'Test Post',
        content: 'Test content',
        author: user,
        ...overrides
      }
    })
  }
  
  static async cleanup() {
    // 按依赖顺序清理数据
    await prisma.$transaction([
      prisma.comment.deleteMany({}),
      prisma.post.deleteMany({}),
      prisma.user.deleteMany({})
    ])
  }
}

// 测试基类
abstract class BaseTestCase {
  protected async beforeEach() {
    await TestDataFactory.cleanup()
  }
  
  protected async afterEach() {
    await TestDataFactory.cleanup()
  }
}

部署和维护最佳实践

数据库迁移策略

bash
# 开发环境迁移流程
npx prisma db push  # 快速应用 schema 变更

# 生产环境迁移流程
npx prisma migrate dev --name feature-name  # 开发时创建迁移
npx prisma migrate deploy                   # 生产环境应用迁移

# 验证迁移
npx prisma migrate status
npx prisma migrate resolve --applied migration-name  # 解决迁移冲突

环境配置

typescript
// 环境特定配置
class PrismaConfig {
  static getClientOptions() {
    const baseOptions = {
      log: ['error']
    }
    
    if (process.env.NODE_ENV === 'development') {
      return {
        ...baseOptions,
        log: ['query', 'info', 'warn', 'error']
      }
    }
    
    if (process.env.NODE_ENV === 'test') {
      return {
        ...baseOptions,
        datasourceUrl: process.env.TEST_DATABASE_URL
      }
    }
    
    return baseOptions
  }
}

通过遵循这些最佳实践,您可以构建更加健壮、安全和高性能的 Prisma 应用程序。