Best Practice
Connection Pooling
In produzione, gestire correttamente le connessioni al database e’ fondamentale.
Configurazione del Pool
# Limita il numero di connessioni
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10&pool_timeout=30"
Singleton in Applicazioni Long-Running
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'warn', 'error']
: ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
Piattaforme Serverless
Per Vercel, AWS Lambda e simili, usa un connection pooler esterno:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // URL con PgBouncer/Supabase Pooler
directUrl = env("DIRECT_URL") // URL diretto per le migrazioni
}
Il Problema N+1
Il problema N+1 e’ uno dei piu’ comuni con gli ORM: si esegue 1 query per ottenere una lista, poi N query aggiuntive per ogni elemento.
Esempio del Problema
// SBAGLIATO: N+1 query
const users = await prisma.user.findMany()
for (const user of users) {
// Ogni iterazione esegue una query separata!
const posts = await prisma.post.findMany({
where: { authorId: user.id },
})
console.log(`${user.name}: ${posts.length} post`)
}
Soluzione con include
// CORRETTO: una singola query con JOIN
const users = await prisma.user.findMany({
include: {
posts: true,
},
})
users.forEach((user) => {
console.log(`${user.name}: ${user.posts.length} post`)
})
Soluzione con _count
Se ti serve solo il conteggio:
// OTTIMALE: usa _count invece di caricare tutti i record
const users = await prisma.user.findMany({
include: {
_count: { select: { posts: true } },
},
})
users.forEach((user) => {
console.log(`${user.name}: ${user._count.posts} post`)
})
Query Optimization
Seleziona Solo i Campi Necessari
// SBAGLIATO: carica tutti i campi
const users = await prisma.user.findMany()
// CORRETTO: carica solo cio' che serve
const users2 = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
},
})
Usa gli Indici
Aggiungi indici sui campi usati frequentemente nei filtri:
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
category String
published Boolean @default(false)
createdAt DateTime @default(now())
@@index([authorId])
@@index([category])
@@index([published, createdAt]) // Indice composto
}
Pagination Efficiente
// Per grandi dataset, preferisci cursor pagination
const posts = await prisma.post.findMany({
take: 20,
skip: 1,
cursor: { id: lastId },
orderBy: { id: 'asc' },
})
Logging
Prisma supporta diversi livelli di log per monitorare le query.
Configurazione Base
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
})
Log con Eventi
const prisma = new PrismaClient({
log: [
{ level: 'query', emit: 'event' },
{ level: 'error', emit: 'stdout' },
{ level: 'warn', emit: 'stdout' },
],
})
// Ascolta le query per monitorare le performance
prisma.$on('query', (e) => {
console.log(`Query: ${e.query}`)
console.log(`Durata: ${e.duration}ms`)
console.log(`Parametri: ${e.params}`)
// Avviso per query lente
if (e.duration > 500) {
console.warn(`QUERY LENTA (${e.duration}ms): ${e.query}`)
}
})
Error Handling
Gestisci correttamente i diversi tipi di errore Prisma:
import { Prisma } from '@prisma/client'
async function createUser(email: string, name: string) {
try {
return await prisma.user.create({
data: { email, name },
})
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2002':
throw new Error(`Il campo ${error.meta?.target} e' gia' in uso`)
case 'P2025':
throw new Error('Record non trovato')
case 'P2003':
throw new Error('Violazione vincolo di chiave esterna')
default:
throw new Error(`Errore database: ${error.code}`)
}
}
if (error instanceof Prisma.PrismaClientValidationError) {
throw new Error('Dati non validi: controlla i campi inviati')
}
if (error instanceof Prisma.PrismaClientInitializationError) {
throw new Error('Impossibile connettersi al database')
}
throw error
}
}
Codici di Errore Comuni
| Codice | Significato |
|---|---|
P2000 |
Valore troppo lungo per il campo |
P2001 |
Record cercato nella condizione WHERE non esiste |
P2002 |
Violazione vincolo di unicita’ |
P2003 |
Violazione chiave esterna |
P2025 |
Record non trovato (update/delete) |
P2034 |
Conflitto nella transazione |
Prisma in Produzione
Checklist per il Deploy
# 1. Genera il client (aggiungi in postinstall)
npx prisma generate
# 2. Applica le migrazioni
npx prisma migrate deploy
# 3. Non usare MAI in produzione:
# - prisma migrate dev (puo' resettare i dati)
# - prisma db push (senza storia delle migrazioni)
# - prisma studio (interfaccia non protetta)
Variabili d’Ambiente
# Produzione
DATABASE_URL="postgresql://user:pass@prod-host:5432/myapp?sslmode=require&connection_limit=15"
Health Check
// Verifica che il database sia raggiungibile
export async function checkDatabaseHealth(): Promise<boolean> {
try {
await prisma.$queryRaw`SELECT 1`
return true
} catch {
return false
}
}
Monitoraggio
Metriche delle Query
// Middleware per tracciare le performance
prisma.$use(async (params, next) => {
const start = Date.now()
const result = await next(params)
const duration = Date.now() - start
console.log(`${params.model}.${params.action} - ${duration}ms`)
if (duration > 1000) {
console.warn(`Query lenta: ${params.model}.${params.action} (${duration}ms)`)
}
return result
})
Soft Delete con Middleware
// Middleware per implementare soft-delete globale
prisma.$use(async (params, next) => {
// Intercetta le operazioni delete e trasformale in update
if (params.action === 'delete') {
params.action = 'update'
params.args.data = { deletedAt: new Date() }
}
// Filtra automaticamente i record eliminati
if (params.action === 'findMany' || params.action === 'findFirst') {
if (!params.args) params.args = {}
if (!params.args.where) params.args.where = {}
params.args.where.deletedAt = null
}
return next(params)
})