| name | dev-prisma-expert |
| description | Expert Prisma ORM pour la conception de schémas, les migrations, l'optimisation de requêtes, la modélisation de relations et les opérations base de données. À utiliser quand l'utilisateur travaille avec Prisma, a des problèmes de schéma, de migration ou de performance de requêtes. Se déclenche aussi avec "prisma", "schema prisma", "migration prisma", "prisma client", "requête prisma lente", "relation prisma". Also triggers on "Prisma schema", "Prisma migration", "Prisma query performance". |
Expert Prisma ORM
Workflow de diagnostic
- Catégoriser : schéma · migration · requête lente · connexion · transaction
- Valider l'état actuel :
npx prisma validate + npx prisma migrate status
- Identifier le root cause : logs SQL (
log: ['query']), EXPLAIN ANALYZE, état drift
- Appliquer la correction : minimal d'abord, vérifier l'impact
- Valider : relancer les commandes de diagnostic, tests d'intégration
Conception de schéma
Modèle canonique
model User {
id String @id @default(cuid()) // ou uuid() selon besoin
email String @unique
role Role @default(USER)
posts Post[] @relation("UserPosts")
profile Profile? @relation("UserProfile")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@index([role, createdAt])
@@map("users")
}
enum Role { USER ADMIN MODERATOR }
model Post {
id String @id @default(cuid())
title String
status PostStatus @default(DRAFT)
authorId String
author User @relation("UserPosts", fields: [authorId], references: [id], onDelete: Cascade)
@@index([authorId])
@@index([status, createdAt])
@@map("posts")
}
Many-to-Many explicite (toujours préférer)
// EVITER : relation implicite (perte de flexibilité)
model Post { tags Tag[] }
model Tag { posts Post[] }
// FAIRE : table de jointure explicite
model PostTag {
postId String
tagId String
addedAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([postId, tagId])
@@map("post_tags")
}
Critères de choix @id
| Cas | Choix |
|---|
| Public, URL-friendly | cuid() ou uuid() |
| Performance max (write-heavy) | Int @id @default(autoincrement()) |
| UUID v7 (ordre temporel) | uuid() + trigger ou app-generated |
| Clé composée | @@id([a, b]) |
Checklist schéma
@relation explicite avec fields + references sur chaque FK
onDelete / onUpdate défini (ne pas laisser le défaut NoAction en prod)
@@index sur chaque FK, chaque champ filtré/trié fréquemment
- Enums pour valeurs fixes (pas de
String ouvert)
@@map / @map pour respecter la convention snake_case de la DB
Migrations
Environnements
npx prisma migrate dev --name add_user_role
npx prisma migrate diff \
--from-schema-datasource prisma/schema.prisma \
--to-schema-datamodel prisma/schema.prisma
npx prisma migrate deploy
npx prisma migrate resolve --applied "20240601_add_user_role"
npx prisma migrate resolve --rolled-back "20240601_add_user_role"
Drift de schéma
npx prisma migrate diff \
--from-schema-datasource prisma/schema.prisma \
--to-migrations ./prisma/migrations
npx prisma db pull
Migration destructive — procédure safe
npx prisma migrate dev --name rename_col --create-only
npx prisma migrate dev
Optimisation des requêtes
Problème N+1 — recette complète
const users = await prisma.user.findMany();
for (const user of users) {
const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}
const users = await prisma.user.findMany({ include: { posts: true } });
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
posts: { select: { id: true, title: true }, take: 5 }
}
});
const [items, total] = await prisma.$transaction([
prisma.post.findMany({ where, skip, take, orderBy }),
prisma.post.count({ where }),
]);
Critère select vs include
select : quand on ne veut qu'un sous-ensemble de champs (performance réseau)
include : quand on veut tous les champs du modèle + la relation
- Les deux ne peuvent pas coexister au même niveau
$queryRaw — quand et comment
import { Prisma } from '@prisma/client';
const stats = await prisma.$queryRaw<{ userId: string; count: bigint }[]>`
SELECT author_id as "userId", COUNT(*) as count
FROM posts
WHERE created_at > ${new Date('2025-01-01')}
GROUP BY author_id
HAVING COUNT(*) > 10
`;
const safeQuery = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM users WHERE id = ${userId}`
);
Activer les logs SQL en dev
const prisma = new PrismaClient({
log: [
{ level: 'query', emit: 'event' },
{ level: 'warn', emit: 'stdout' },
],
});
prisma.$on('query', (e) => {
console.log(`Query: ${e.query} — ${e.duration}ms`);
});
Gestion des connexions
Singleton (Node.js / Next.js)
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'warn'] : ['warn'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
Serverless / Edge (Vercel, Cloudflare)
import { PrismaClient } from '@prisma/client/edge';
import { withAccelerate } from '@prisma/extension-accelerate';
const prisma = new PrismaClient().$extends(withAccelerate());
const user = await prisma.user.findUnique({
where: { id },
cacheStrategy: { ttl: 60 },
});
Pool de connexions — valeurs recommandées
# connection_limit = (nb_CPU * 2) + 1 en général
DATABASE_URL="postgresql://user:pass@host/db?connection_limit=10&pool_timeout=15"
Transactions
const [user, account] = await prisma.$transaction([
prisma.user.create({ data: userData }),
prisma.account.create({ data: accountData }),
]);
const result = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({ data: userData });
if (!user) throw new Error('User creation failed');
await tx.auditLog.create({ data: { action: 'USER_CREATED', userId: user.id } });
return user;
}, {
maxWait: 5000,
timeout: 10000,
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
});
Pièges et anti-patterns
| Anti-pattern | Problème | Correction |
|---|
migrate dev en production | Réinitialise la DB si drift | Utiliser migrate deploy |
include sans where sur relation large | Charge tout en mémoire | Ajouter take + where |
| Many-to-many implicite | Impossible d'ajouter des champs à la relation | Table de jointure explicite |
$queryRaw avec concaténation string | Injection SQL | Template literals Prisma.sql |
| Instancier PrismaClient dans chaque handler | Épuisement du pool | Singleton global |
Omettre onDelete | Erreur FK en cascade ou orphelins | Définir Cascade / SetNull |
| Index manquant sur FK | Full table scan à chaque JOIN | @@index([foreignKeyField]) |
Enum modifié via db push en prod | Risque de perte de données | Migration SQL explicite |
Commandes essentielles
npx prisma generate
npx prisma validate
npx prisma format
npx prisma migrate status
npx prisma studio
npx prisma db push
npx prisma db seed
Communication Rules — MANDATORY
- Ultra-concise. No filler, no preamble, no pleasantries.
- Never say "happy to help", "sure!", "great question", "let me", or similar.
- Tool first, talk second. Act before explaining.
- Result first. Lead with outcome, not process.
- Stop when done. No summary, no recap, no trailing commentary.
- No politeness wrappers. Direct and blunt.
- Minimum words. If one word works, do not use ten.
- No unsolicited explanations.
- No emoji unless asked.