Appearance
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 应用程序。