Nuxt: 数据库 Prisma
最后更新:2026-08-26
MegaShop 一直用 mock 数据——商品数据存在 JS 数组里,重启就丢失。Charlie 需要真正的数据库。Bob 写的原生 SQL 又容易出错、没有类型安全。Prisma ORM 提供类型安全的数据库查询,一行代码就能生成 TypeScript 类型。
1. 你将学到
- Prisma 安装与初始化:schema.prisma + generate + migrate
- 数据模型设计:Product/Category/User/Order/CartItem 表关系
- Nuxt 集成:server/utils/prisma.ts 单例连接
- CRUD 操作:findMany/create/update/delete + 事务 + 聚合
- MegaShop 百万级商品分页查询 + 订单事务
2. 一个架构师的真实故事
(1) 痛点:Mock 数据无法持久化
Bob 重启 MegaShop 开发服务器后,所有商品数据消失。Alice 加的商品、下的订单全没了。生产环境更是没有数据库——百万商品数据无处存放。
(2) Prisma ORM 的解法
Prisma 提供类型安全的数据库操作,自动生成 TypeScript 类型:
TYPESCRIPT
// Type-safe query - no raw SQL
const products = await prisma.product.findMany({
where: { category: { slug: 'electronics' } },
take: 20,
skip: (page - 1) * 20
})
(3) 收益:类型安全 + 数据持久
商品数据持久存储,API 查询有完整类型推导,Bob 再也不会写错字段名,Alice 的订单重启后还在。
3. Prisma 安装与初始化
▶ 示例:安装 Prisma
BASH
npm install prisma @prisma/client
npx prisma init
输出:
TEXT
📖 仅展示
# 命令执行成功
▶ 示例:Prisma Schema
PRISMA
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Product {
id Int @id @default(autoincrement())
name String
slug String @unique
description String?
price Decimal @db.Decimal(10, 2)
image String?
inStock Boolean @default(true)
rating Float @default(0)
reviewCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
orderItems OrderItem[]
cartItems CartItem[]
@@index([categoryId])
@@index([slug])
@@index([price])
}
model Category {
id Int @id @default(autoincrement())
name String
slug String @unique
parentId Int?
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
children Category[] @relation("CategoryTree")
products Product[]
@@index([slug])
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String?
avatar String?
provider String @default("email")
role Role @default(CUSTOMER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
cartItems CartItem[]
@@index([email])
}
model Order {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
total Decimal @db.Decimal(10, 2)
status OrderStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items OrderItem[]
@@index([userId])
@@index([status])
}
model OrderItem {
id Int @id @default(autoincrement())
orderId Int
order Order @relation(fields: [orderId], references: [id])
productId Int
product Product @relation(fields: [productId], references: [id])
quantity Int
price Decimal @db.Decimal(10, 2)
@@index([orderId])
}
model CartItem {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
productId Int
product Product @relation(fields: [productId], references: [id])
quantity Int @default(1)
@@unique([userId, productId])
}
enum Role {
CUSTOMER
ADMIN
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
输出:
TEXT
📖 仅展示
// 执行成功
(1) MegaShop ER 图
erDiagram
Product ||--o{ OrderItem : "included in"
Product ||--o{ CartItem : "added to"
Product }o--|| Category : "belongs to"
Category ||--o{ Category : "parent-child"
User ||--o{ Order : "places"
User ||--o{ CartItem : "has"
Order ||--o{ OrderItem : "contains"
4. Nuxt 集成
▶ 示例:Prisma 单例连接
TYPESCRIPT
// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client'
// Singleton pattern - prevent multiple instances in dev
const globalForPrisma = globalThis 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
}
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:Nitro 预生成 Prisma Client
TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
// Generate Prisma Client before build
externals: {
inline: ['.prisma/client']
}
},
hooks: {
'build:before': async () => {
const { execSync } = await import('child_process')
execSync('npx prisma generate')
}
}
})
输出:
TEXT
📖 仅展示
// 执行成功
5. CRUD 操作
▶ 示例:商品列表分页查询
TYPESCRIPT
// server/api/products/index.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const page = Number(query.page) || 1
const limit = Number(query.limit) || 20
const category = query.category as string
const search = query.search as string
const minPrice = Number(query.minPrice) || 0
const maxPrice = Number(query.maxPrice) || Infinity
const where = {
AND: [
category ? { category: { slug: category } } : {},
search ? { name: { contains: search, mode: 'insensitive' as const } } : {},
{ price: { gte: minPrice } },
maxPrice < Infinity ? { price: { lte: maxPrice } } : {}
]
}
const [items, total] = await Promise.all([
prisma.product.findMany({
where,
skip: (page - 1) * limit,
take: limit,
include: { category: { select: { name: true, slug: true } } },
orderBy: { createdAt: 'desc' }
}),
prisma.product.count({ where })
])
return { items, total, page, limit }
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:商品详情查询
TYPESCRIPT
// server/api/products/[id].get.ts
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'))
const product = await prisma.product.findUnique({
where: { id },
include: {
category: { select: { name: true, slug: true } }
}
})
if (!product) {
throw createError({ statusCode: 404, message: 'Product not found' })
}
return product
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:创建订单(事务)
TYPESCRIPT
// server/api/orders/index.post.ts
export default defineEventHandler(async (event) => {
const userId = event.context.user?.id
const { items } = await readBody(event)
// Transaction: create order + update stock + clear cart
const order = await prisma.$transaction(async (tx) => {
// Calculate total
let total = 0
const orderItems = []
for (const item of items) {
const product = await tx.product.findUnique({ where: { id: item.productId } })
if (!product || !product.inStock) {
throw createError({ statusCode: 400, message: `Product ${item.productId} unavailable` })
}
total += Number(product.price) * item.quantity
orderItems.push({
productId: product.id,
quantity: item.quantity,
price: product.price
})
}
// Create order
const newOrder = await tx.order.create({
data: {
userId,
total,
items: { create: orderItems }
},
include: { items: { include: { product: true } } }
})
// Clear user cart
await tx.cartItem.deleteMany({ where: { userId } })
return newOrder
})
return { order, message: 'Order created successfully' }
})
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:聚合查询——商品统计
TYPESCRIPT
// server/api/admin/stats.get.ts
export default defineEventHandler(async () => {
const [
totalProducts,
totalUsers,
totalOrders,
revenue,
avgPrice
] = await Promise.all([
prisma.product.count(),
prisma.user.count(),
prisma.order.count(),
prisma.order.aggregate({
_sum: { total: true },
where: { status: 'PAID' }
}),
prisma.product.aggregate({
_avg: { price: true }
})
])
return {
totalProducts,
totalUsers,
totalOrders,
totalRevenue: revenue._sum.total || 0,
averagePrice: avgPrice._avg.price || 0
}
})
输出:
TEXT
📖 仅展示
// 执行成功
6. 数据库迁移
▶ 示例:Prisma 迁移命令
BASH
# Create migration from schema changes
npx prisma migrate dev --name init
# Apply migrations in production
npx prisma migrate deploy
# Reset database (dev only!)
npx prisma migrate reset
# Generate Prisma Client
npx prisma generate
# Open Prisma Studio (visual DB browser)
npx prisma studio
# Seed database with test data
npx prisma db seed
输出:
TEXT
📖 仅展示
# 命令执行成功
▶ 示例:种子数据
TYPESCRIPT
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// Create categories
const electronics = await prisma.category.create({
data: { name: 'Electronics', slug: 'electronics' }
})
const clothing = await prisma.category.create({
data: { name: 'Clothing', slug: 'clothing' }
})
// Create products
for (let i = 1; i <= 1000; i++) {
await prisma.product.create({
data: {
name: `Product ${i}`,
slug: `product-${i}`,
price: Math.round(Math.random() * 500 * 100) / 100,
categoryId: i % 2 === 0 ? electronics.id : clothing.id,
inStock: Math.random() > 0.2,
rating: Math.round(Math.random() * 5 * 10) / 10,
reviewCount: Math.floor(Math.random() * 5000)
}
})
}
// Create admin user
await prisma.user.create({
data: {
email: 'bob@megashop.com',
name: 'Bob Admin',
role: 'ADMIN'
}
})
console.log('Seed completed: 1 thousand products + 2 categories + 1 admin')
}
main()
输出:
TEXT
📖 仅展示
// 执行成功
7. 综合示例:MegaShop 数据库集成
TEXT
📖 仅展示
# .env
DATABASE_URL="postgresql://megashop:password@localhost:5432/megashop"
REDIS_URL="redis://localhost:6379"
JWT_ACCESS_SECRET="your-access-secret"
JWT_REFRESH_SECRET="your-refresh-secret"
TYPESCRIPT
// server/api/products/search.get.ts - Advanced search
export default defineEventHandler(async (event) => {
const { q, category, minPrice, maxPrice, sort, page, limit } = getQuery(event)
const where = {
AND: [
q ? { OR: [{ name: { contains: q as string, mode: 'insensitive' } }, { description: { contains: q as string, mode: 'insensitive' } }] } : {},
category ? { category: { slug: category as string } } : {},
minPrice ? { price: { gte: Number(minPrice) } } : {},
maxPrice ? { price: { lte: Number(maxPrice) } } : {}
]
}
const orderBy: any = sort === 'price-asc' ? { price: 'asc' }
: sort === 'price-desc' ? { price: 'desc' }
: sort === 'rating' ? { rating: 'desc' }
: { createdAt: 'desc' }
const [items, total] = await Promise.all([
prisma.product.findMany({
where, orderBy,
skip: ((Number(page) || 1) - 1) * (Number(limit) || 20),
take: Number(limit) || 20,
include: { category: { select: { name: true, slug: true } } }
}),
prisma.product.count({ where })
])
return { items, total, page: Number(page) || 1, limit: Number(limit) || 20 }
})
❓ 常见问题
Q Prisma 和 TypeORM 有什么区别?
A Prisma 用声明式 Schema + 生成客户端,类型推导更完整。TypeORM 用装饰器,更接近传统 ORM。Nuxt 3 社区更推荐 Prisma。
Q Prisma 单例为什么需要 global 变量?
A 开发环境 Nuxt 热更新会重新执行 server 代码,不用 global 会创建多个 PrismaClient 实例导致连接泄漏。生产环境不存在此问题。
Q 百万级数据查询性能如何?
A Prisma 的 findMany + skip/take 生成 LIMIT/OFFSET 分页,百万级数据 OFFSET 大时慢。大数据量用 cursor-based 分页(cursor + take)。
Q Prisma 支持 MongoDB 吗?
A 支持,但作为预览功能。PostgreSQL 是 Prisma 的最佳搭档,功能最完整。MegaShop 推荐 PostgreSQL。
Q 事务中能否执行多个写操作?
A 可以。prisma.$transaction 内可以做任意数量的读写操作,全部成功才提交,任一失败则回滚。
Q seed 脚本怎么执行?
A 在 package.json 加
"prisma": { "seed": "npx ts-node prisma/seed.ts" },然后运行 npx prisma db seed。📖 小节
- Prisma 提供 Schema 定义 + 自动迁移 + 类型安全查询 + 可视化 Studio
- server/utils/prisma.ts 实现单例连接,防止开发环境连接泄漏
- CRUD:findMany 分页 + findUnique 详情 + create + update + delete
- $transaction 事务保证订单创建 + 库存更新 + 购物车清空原子性
- 百万级数据:索引优化 + cursor 分页 + 聚合查询统计
📝 作业
- 基础题(难度⭐):安装 Prisma + PostgreSQL,创建 Product/Category 模型,运行 migrate + seed
- 进阶题(难度⭐⭐):实现完整的商品 CRUD API,替换之前的 mock 数据,支持分页和分类筛选
- 挑战题(难度⭐⭐⭐):实现订单事务——创建订单 + 扣库存 + 清购物车在一个事务中完成,任何一步失败全部回滚
---|