Backend
Prisma ORM
ORM type-safe para Node.js e TypeScript com migrations e client generator
O que é Prisma?
Prisma é um ORM (Object-Relational Mapping) moderno para Node.js e TypeScript que substitui ORMs tradicionais. Consiste em três ferramentas principais: Prisma Client (type-safe query builder), Prisma Migrate (sistema de migrações) e Prisma Studio (GUI para banco de dados).
Por que utilizamos Prisma na IngenioLab?
- Type Safety: Tipos TypeScript gerados automaticamente
- Developer Experience: Autocompletar inteligente e IntelliSense
- Query Performance: Queries otimizadas e lazy loading
- Schema-First: Schema único como fonte da verdade
- Multi-database: Suporte para PostgreSQL, MySQL, SQLite, SQL Server
- Migrations: Sistema de migrações robusto e versionado
Instalação e Configuração
1. Instalação:
# Prisma CLI e clientnpm install prisma @prisma/client# Inicializar projeto Prismanpx prisma init
2. Estrutura gerada:
projeto/├── prisma/│ ├── schema.prisma # Schema do banco│ ├── migrations/ # Migrações SQL│ └── seed.ts # Script de seed├── .env # Variáveis de ambiente└── node_modules/└── .prisma/ # Cliente gerado
3. Configuração inicial do schema:
// prisma/schema.prismagenerator client {provider = "prisma-client-js"output = "../node_modules/.prisma/client"}datasource db {provider = "postgresql" // ou "mysql", "sqlite"url = env("DATABASE_URL")}
4. Variável de ambiente:
# .envDATABASE_URL="postgresql://user:password@localhost:5432/ingeniolab?schema=public"
Schema Prisma - Padrões IngenioLab
1. Modelos Básicos:
// prisma/schema.prismamodel User {id String @id @default(cuid())email String @uniquename String?password Stringrole UserRole @default(USER)isActive Boolean @default(true)// TimestampscreatedAt DateTime @default(now())updatedAt DateTime @updatedAt// Relacionamentosposts Post[]profile Profile?@@map("users") // Nome da tabela no banco}model Profile {id String @id @default(cuid())bio String?avatar String?website String?// Foreign KeyuserId String @uniqueuser User @relation(fields: [userId], references: [id], onDelete: Cascade)createdAt DateTime @default(now())updatedAt DateTime @updatedAt@@map("profiles")}model Post {id String @id @default(cuid())title Stringcontent String?slug String @uniquepublished Boolean @default(false)publishedAt DateTime?// Foreign KeysauthorId Stringauthor User @relation(fields: [authorId], references: [id])categoryId String?category Category? @relation(fields: [categoryId], references: [id])// Many-to-manytags Tag[]createdAt DateTime @default(now())updatedAt DateTime @updatedAt@@map("posts")@@index([slug])@@index([authorId])}model Category {id String @id @default(cuid())name String @uniquedescription String?posts Post[]@@map("categories")}model Tag {id String @id @default(cuid())name String @uniqueposts Post[]@@map("tags")}enum UserRole {USERADMINMODERATOR}
2. Relacionamentos Avançados:
model Company {id String @id @default(cuid())name Stringcnpj String @uniqueemployees Employee[]projects Project[]@@map("companies")}model Employee {id String @id @default(cuid())name Stringemail String @uniqueposition Stringsalary DecimalcompanyId Stringcompany Company @relation(fields: [companyId], references: [id])// Self-relation (manager)managerId String?manager Employee? @relation("ManagerEmployee", fields: [managerId], references: [id])subordinates Employee[] @relation("ManagerEmployee")// Many-to-many com tabela intermediáriaprojectAssignments ProjectAssignment[]@@map("employees")}model Project {id String @id @default(cuid())name Stringdescription String?startDate DateTimeendDate DateTime?budget Decimal?companyId Stringcompany Company @relation(fields: [companyId], references: [id])assignments ProjectAssignment[]@@map("projects")}// Tabela intermediária com campos extrasmodel ProjectAssignment {id String @id @default(cuid())role StringhourlyRate Decimal?startDate DateTime @default(now())endDate DateTime?employeeId Stringemployee Employee @relation(fields: [employeeId], references: [id])projectId Stringproject Project @relation(fields: [projectId], references: [id])@@unique([employeeId, projectId])@@map("project_assignments")}
Cliente Prisma - Instanciação
1. Configuração do Cliente:
// src/lib/prisma.tsimport { PrismaClient } from '@prisma/client'import { env } from '../utils/env'// Singleton pattern para evitar múltiplas instânciasconst globalForPrisma = globalThis as unknown as {prisma: PrismaClient | undefined}export const prisma = globalForPrisma.prisma ??new PrismaClient({log: env.NODE_ENV === 'development'? ['query', 'error', 'warn']: ['error'],datasources: {db: {url: env.DATABASE_URL}}})if (env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma// Graceful shutdownprocess.on('beforeExit', async () => {await prisma.$disconnect()})export default prisma
2. Extensões do Cliente (Middleware):
// src/lib/prisma-extended.tsimport { PrismaClient } from '@prisma/client'import bcrypt from 'bcryptjs'const prisma = new PrismaClient()// Middleware para hash de senhasprisma.$use(async (params, next) => {if (params.model === 'User') {if (params.action === 'create' || params.action === 'update') {const data = params.args.dataif (data.password) {data.password = await bcrypt.hash(data.password, 10)}}}return next(params)})// Middleware para soft deleteprisma.$use(async (params, next) => {// Interceptar delete e transformar em updateif (params.action === 'delete') {params.action = 'update'params.args.data = { deletedAt: new Date() }}// Filtrar registros deletados em find operationsif (params.action === 'findMany' || params.action === 'findFirst') {if (!params.args.where) params.args.where = {}params.args.where.deletedAt = null}return next(params)})export { prisma }
Operações CRUD com Padrões
1. Service Layer Pattern:
// src/services/userService.tsimport { prisma } from '../lib/prisma'import { Prisma } from '@prisma/client'import bcrypt from 'bcryptjs'export class UserService {// CREATEasync createUser(data: Prisma.UserCreateInput) {try {const user = await prisma.user.create({data,select: {id: true,email: true,name: true,role: true,createdAt: true}})return user} catch (error) {if (error instanceof Prisma.PrismaClientKnownRequestError) {if (error.code === 'P2002') {throw new Error('Email já está em uso')}}throw error}}// READ with filtering and paginationasync getUsers(params: {skip?: numbertake?: numberwhere?: Prisma.UserWhereInputorderBy?: Prisma.UserOrderByWithRelationInput}) {const { skip, take, where, orderBy } = paramsconst [users, total] = await prisma.$transaction([prisma.user.findMany({skip,take,where,orderBy,select: {id: true,email: true,name: true,role: true,isActive: true,createdAt: true,_count: {select: { posts: true }}}}),prisma.user.count({ where })])return {users,pagination: {total,page: Math.floor((skip || 0) / (take || 10)) + 1,limit: take || 10,hasNext: (skip || 0) + (take || 10) < total}}}// READ by ID with relationsasync getUserById(id: string) {const user = await prisma.user.findUnique({where: { id },include: {profile: true,posts: {where: { published: true },orderBy: { createdAt: 'desc' },take: 5},_count: {select: {posts: true}}}})if (!user) {throw new Error('Usuário não encontrado')}return user}// UPDATEasync updateUser(id: string, data: Prisma.UserUpdateInput) {try {const user = await prisma.user.update({where: { id },data,select: {id: true,email: true,name: true,role: true,updatedAt: true}})return user} catch (error) {if (error instanceof Prisma.PrismaClientKnownRequestError) {if (error.code === 'P2025') {throw new Error('Usuário não encontrado')}}throw error}}// DELETE (soft delete)async deleteUser(id: string) {await prisma.user.update({where: { id },data: {isActive: false,deletedAt: new Date()}})}// Operações complexasasync authenticateUser(email: string, password: string) {const user = await prisma.user.findUnique({where: { email },select: {id: true,email: true,password: true,name: true,role: true,isActive: true}})if (!user || !user.isActive) {throw new Error('Credenciais inválidas')}const isValidPassword = await bcrypt.compare(password, user.password)if (!isValidPassword) {throw new Error('Credenciais inválidas')}// Não retornar a senhaconst { password: _, ...userWithoutPassword } = userreturn userWithoutPassword}}export const userService = new UserService()
2. Queries Avançadas:
// src/services/postService.tsimport { prisma } from '../lib/prisma'import { Prisma } from '@prisma/client'export class PostService {// Busca com full-text searchasync searchPosts(query: string, filters: {categoryId?: stringauthorId?: stringpublished?: booleantags?: string[]}) {const posts = await prisma.post.findMany({where: {AND: [{OR: [{ title: { contains: query, mode: 'insensitive' } },{ content: { contains: query, mode: 'insensitive' } }]},{ published: filters.published ?? true },filters.categoryId ? { categoryId: filters.categoryId } : {},filters.authorId ? { authorId: filters.authorId } : {},filters.tags ? {tags: {some: {name: { in: filters.tags }}}} : {}]},include: {author: {select: { name: true, email: true }},category: {select: { name: true }},tags: {select: { name: true }}},orderBy: [{ publishedAt: 'desc' },{ createdAt: 'desc' }]})return posts}// Agregações e estatísticasasync getPostStats() {const stats = await prisma.post.aggregate({_count: {id: true},_avg: {// Se tivesse campo views// views: true},where: {published: true}})const postsByCategory = await prisma.post.groupBy({by: ['categoryId'],_count: {id: true},where: {published: true}})const topAuthors = await prisma.user.findMany({select: {id: true,name: true,_count: {select: {posts: {where: { published: true }}}}},orderBy: {posts: {_count: 'desc'}},take: 10})return {totalPosts: stats._count.id,postsByCategory,topAuthors}}// Raw queries quando necessárioasync getPostsWithCustomSQL() {const posts = await prisma.$queryRaw`SELECT p.*, u.name as author_name, c.name as category_nameFROM posts pLEFT JOIN users u ON p.author_id = u.idLEFT JOIN categories c ON p.category_id = c.idWHERE p.published = trueORDER BY p.created_at DESCLIMIT 10`return posts}}
Migrations e Schema Evolution
1. Comandos de Migration:
# Gerar e aplicar migrationnpx prisma migrate dev --name add-user-profile# Aplicar migrations em produçãonpx prisma migrate deploy# Reset do banco (desenvolvimento)npx prisma migrate reset# Visualizar estado das migrationsnpx prisma migrate status# Resolver conflitos de migrationnpx prisma migrate resolve --applied "20231201000000_migration_name"
2. Schema Changes - Exemplo de evolução:
// Adicionando campos opcionais (safe)model User {id String @id @default(cuid())email String @uniquename String?password String// Novos campos opcionaisphone String? // ✅ Safe - campo opcionalavatar String? // ✅ Safe - campo opcionalsettings Json? // ✅ Safe - campo opcionalcreatedAt DateTime @default(now())updatedAt DateTime @updatedAt@@map("users")}// Adicionando campos obrigatórios (requer default ou migration manual)model User {id String @id @default(cuid())email String @uniquename String?password String// Campo obrigatório novo - precisa de defaultstatus UserStatus @default(ACTIVE) // ✅ Com default@@map("users")}enum UserStatus {ACTIVEINACTIVESUSPENDED}
3. Data Migrations (Seeds):
// prisma/seed.tsimport { PrismaClient } from '@prisma/client'import bcrypt from 'bcryptjs'const prisma = new PrismaClient()async function main() {// Limpar dados existentes (desenvolvimento)await prisma.post.deleteMany()await prisma.user.deleteMany()await prisma.category.deleteMany()// Criar categoriasconst categories = await prisma.$transaction([prisma.category.create({data: { name: 'Tecnologia', description: 'Posts sobre tecnologia' }}),prisma.category.create({data: { name: 'Design', description: 'Posts sobre design' }})])// Criar usuáriosconst adminUser = await prisma.user.create({data: {email: 'admin@ingeniolab.com',name: 'Admin User',password: await bcrypt.hash('admin123', 10),role: 'ADMIN'}})// Criar postsawait prisma.post.createMany({data: [{title: 'Primeiro Post',content: 'Conteúdo do primeiro post',slug: 'primeiro-post',published: true,authorId: adminUser.id,categoryId: categories[0].id},{title: 'Segundo Post',content: 'Conteúdo do segundo post',slug: 'segundo-post',published: false,authorId: adminUser.id,categoryId: categories[1].id}]})console.log('Seed completed!')}main().catch((e) => {console.error(e)process.exit(1)}).finally(async () => {await prisma.$disconnect()})
4. Executar seed:
# Executar seednpx prisma db seed# Configurar no package.json{"prisma": {"seed": "tsx prisma/seed.ts"}}
Otimizações e Performance
1. Connection Pooling:
// src/lib/prisma.tsimport { PrismaClient } from '@prisma/client'export const prisma = new PrismaClient({datasources: {db: {url: process.env.DATABASE_URL}}})// Para aplicações serverless (Lambda, Vercel)export const prismaEdge = new PrismaClient({datasources: {db: {url: process.env.DATABASE_URL}}})
2. Query Optimization:
// ❌ N+1 Problemconst users = await prisma.user.findMany()for (const user of users) {const posts = await prisma.post.findMany({where: { authorId: user.id }})}// ✅ Include/Select optimizationconst users = await prisma.user.findMany({include: {posts: {select: {id: true,title: true,published: true}}}})// ✅ Usando select apenas campos necessáriosconst users = await prisma.user.findMany({select: {id: true,name: true,email: true,posts: {select: {id: true,title: true},where: { published: true },take: 5}}})
3. Índices Customizados:
model Post {id String @id @default(cuid())title Stringcontent String?published Boolean @default(false)authorId String@@index([published, authorId]) // Índice composto@@index([title(ops: raw("gin_trgm_ops"))], type: Gin) // Full-text search (PostgreSQL)@@fulltext([title, content]) // Full-text search (MySQL)@@map("posts")}
Debugging e Monitoring
1. Query Logging:
const prisma = new PrismaClient({log: [{ level: 'query', emit: 'event' },{ level: 'error', emit: 'stdout' },{ level: 'info', emit: 'stdout' },{ level: 'warn', emit: 'stdout' }]})prisma.$on('query', (e) => {console.log('Query: ' + e.query)console.log('Params: ' + e.params)console.log('Duration: ' + e.duration + 'ms')})
2. Prisma Studio:
# Abrir interface visual do banconpx prisma studio