Prisma Database
MegaShop sempre usou dados mock—dados de produto armazenados em arrays JavaScript, que são perdidos ao reiniciar. Charlie precisa de um banco de dados real. O SQL nativo escrito por Bob é propenso a erros e carece de segurança de tipos. O Prisma ORM fornece consultas de banco de dados type-safe e gera tipos TypeScript com apenas uma linha de código.
1. O Que Você Vai Aprender
- Instalação e Inicialização do Prisma: schema.prisma + generate + migrate
- Design de Modelo de Dados: Relacionamentos entre as tabelas Product, Category, User, Order e CartItem
- Integração com Nuxt: Conexão Singleton em server/utils/prisma.ts
- Operações CRUD: findMany/create/update/delete + transações + agregações
- MegaShop: Busca Paginada para Milhões de Produtos + Transações de Pedido
2. Uma História Real de um Arquiteto
(1) Dor: Dados mock não podem ser persistidos
Após Bob reiniciar o servidor de desenvolvimento do MegaShop, todos os dados de produto desapareceram. Todos os produtos que Alice havia adicionado e os pedidos que ela havia feito sumiram. Para piorar, não havia banco de dados no ambiente de produção—não havia onde armazenar os dados de milhões de produtos.
(2) Solução com Prisma ORM
O Prisma fornece operações de banco de dados type-safe e gera automaticamente tipos TypeScript:
// Consulta type-safe - sem SQL bruto
const products = await prisma.product.findMany({
where: { category: { slug: 'electronics' } },
take: 20,
skip: (page - 1) * 20
})
(3) Benefícios: Segurança de Tipos + Persistência de Dados
Os dados de produto são armazenados de forma persistente, e as consultas de API possuem inferência de tipos completa—então Bob nunca mais vai digitar errado um nome de campo, e o pedido de Alice ainda estará lá após uma reinicialização.
3. Instalação e Inicialização do Prisma
(1) ▶ Exemplo: Instalando o Prisma
npm install prisma @prisma/client
npx prisma init
Saída:
# Comando executado com sucesso
(2) ▶ Exemplo: Schema do 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
}
Saída:
// Execução bem-sucedida
(1) Diagrama ER do MegaShop
erDiagram
Product ||--o{ OrderItem : "incluído em"
Product ||--o{ CartItem : "adicionado ao"
Product }o--|| Category : "pertence a"
Category ||--o{ Category : "pai-filho"
User ||--o{ Order : "faz"
User ||--o{ CartItem : "tem"
Order ||--o{ OrderItem : "contém"
4. Integração com Nuxt
(1) ▶ Exemplo: Conexão Singleton do Prisma
// server/utils/prisma.ts
import { PrismaClient } from '@prisma/client'
// Padrão Singleton - previne múltiplas instâncias em 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
}
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Nitro Pre-built Prisma Client
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
// Gerar Prisma Client antes do build
externals: {
inline: ['.prisma/client']
}
},
hooks: {
'build:before': async () => {
const { execSync } = await import('child_process')
execSync('npx prisma generate')
}
}
})
Saída:
// Execução bem-sucedida
5. Operações CRUD
(1) ▶ Exemplo: Consultas Paginadas para Lista de Produtos
// 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 }
})
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Consulta de Detalhes do Produto
// 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
})
Saída:
// Execução bem-sucedida
(3) ▶ Exemplo: Criando um Pedido (Transação)
// server/api/orders/index.post.ts
export default defineEventHandler(async (event) => {
const userId = event.context.user?.id
const { items } = await readBody(event)
// Transação: criar pedido + atualizar estoque + limpar carrinho
const order = await prisma.$transaction(async (tx) => {
// Calcular 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
})
}
// Criar pedido
const newOrder = await tx.order.create({
data: {
userId,
total,
items: { create: orderItems }
},
include: { items: { include: { product: true } } }
})
// Limpar carrinho do usuário
await tx.cartItem.deleteMany({ where: { userId } })
return newOrder
})
return { order, message: 'Order created successfully' }
})
Saída:
// Execução bem-sucedida
(4) ▶ Exemplo: Consulta de Agregação—Estatísticas de Produto
// 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
}
})
Saída:
// Execução bem-sucedida
6. Migração de Banco de Dados
(1) ▶ Exemplo: Comando de migração do Prisma
# Criar migração a partir de mudanças no schema
npx prisma migrate dev --name init
# Aplicar migrações em produção
npx prisma migrate deploy
# Resetar banco de dados (apenas dev!)
npx prisma migrate reset
# Gerar Prisma Client
npx prisma generate
# Abrir Prisma Studio (navegador visual de DB)
npx prisma studio
# Popular banco de dados com dados de teste
npx prisma db seed
Saída:
# Comando executado com sucesso
(2) ▶ Exemplo: Dados Seed
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// Criar categorias
const electronics = await prisma.category.create({
data: { name: 'Electronics', slug: 'electronics' }
})
const clothing = await prisma.category.create({
data: { name: 'Clothing', slug: 'clothing' }
})
// Criar produtos
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)
}
})
}
// Criar usuário administrador
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()
Saída:
// Execução bem-sucedida
7. Exemplo Completo: Integração de Banco de Dados do MegaShop
# .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"
// server/api/products/search.get.ts - Busca avançada
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 }
})
❓ Perguntas Frequentes
findMany do Prisma combinado com skip/take gera paginação LIMIT/OFFSET, que fica lenta quando o valor de OFFSET é grande para conjuntos de dados na casa dos milhões. Para grandes conjuntos de dados, use paginação baseada em cursor (cursor + take).prisma.$transaction; a transação é commitada apenas se todas as operações forem bem-sucedidas, e revertida se qualquer uma falhar."prisma": { "seed": "npx ts-node prisma/seed.ts" } ao package.json, depois execute npx prisma db seed.📖 Resumo
- Prisma oferece definições de schema, migração automatizada, consultas type-safe e um Studio visual
- server/utils/prisma.ts implementa uma conexão singleton para prevenir vazamentos de conexão no ambiente de desenvolvimento
- CRUD: findMany com paginação + findUnique para detalhes + create + update + delete
- $transaction garante que criação de pedido, atualização de estoque e limpeza de carrinho sejam realizadas atomicamente
- Conjuntos de dados na casa dos milhões: Otimização de índices + paginação por cursor + consultas de agregação para estatísticas
📝 Exercícios
- Exercício Básico (Dificuldade ⭐): Instale Prisma e PostgreSQL, crie os modelos Product e Category, e execute
migrateeseed. - Exercício Avançado (Dificuldade: ⭐⭐): Implemente uma API CRUD completa de produto, substituindo os dados mock anteriores, e suporte paginação e filtragem por categoria.
- Desafio (Dificuldade: ⭐⭐⭐): Implemente uma transação de pedido—criar pedido, deduzir estoque e limpar carrinho—tudo em uma única transação; se qualquer etapa falhar, reverta toda a transação.
---|



