Nuxt: 数据库 Prisma

最后更新:2026-08-26

MegaShop 一直用 mock 数据——商品数据存在 JS 数组里,重启就丢失。Charlie 需要真正的数据库。Bob 写的原生 SQL 又容易出错、没有类型安全。Prisma ORM 提供类型安全的数据库查询,一行代码就能生成 TypeScript 类型。

1. 你将学到


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 图

100%
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

📖 小节


📝 作业

  1. 基础题(难度⭐):安装 Prisma + PostgreSQL,创建 Product/Category 模型,运行 migrate + seed
  2. 进阶题(难度⭐⭐):实现完整的商品 CRUD API,替换之前的 mock 数据,支持分页和分类筛选
  3. 挑战题(难度⭐⭐⭐):实现订单事务——创建订单 + 扣库存 + 清购物车在一个事务中完成,任何一步失败全部回滚

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏