Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

9.3 — ORM & query builders

🎯 Objectif : utiliser un ORM en confiance, savoir descendre en SQL brut quand nécessaire, et toujours détecter le problème N+1.

À l'issue de cet axe, tu sauras :

  • Distinguer ORM (Active Record vs Data Mapper) et query builder
  • Choisir Prisma 7, Drizzle, SQLAlchemy, Eloquent, Doctrine selon le contexte
  • Identifier et fixer un problème N+1
  • Utiliser des transactions, des bulks inserts, du raw SQL quand pertinent
  • Configurer migrations, seeds, factories

Confirmé 10 min prérequis : axe 8 lu

NiveauExempleForces
Raw SQLdb.exec('SELECT * FROM users WHERE id = $1', [42])Contrôle total, perfs maximales
Query builderdb.select().from(users).where(eq(users.id, 42))Composabilité, type-safety, plus lisible que SQL string
ORM Active RecordUser.find(42), user.postsProductivité, relations magiques
ORM Data MapperentityManager.find(User, 42)Séparation domaine ↔ persistance, propre en DDD

Tu peux mixer les niveaux — tous les ORMs modernes permettent du raw SQL quand nécessaire.

OutilStyleNote
Prisma 7Schema-first, ORMProductivité maximale, doc top, engine TS/WASM depuis v7
DrizzleQuery builder TS-firstEdge-ready, proche du SQL, bundle minuscule
KyselyPure query builderType-safety extrême, pas d’ORM
TypeORMActive Record / Data MapperStyle Java, NestJS-friendly, en perte de vitesse
MikroORMData Mapper proche de DoctrineNiche, utile si tu viens de PHP
OutilStyleNote
SQLAlchemy 2Data Mapper, sync ou asyncLe plus puissant, verbose
SQLModelSQLAlchemy + PydanticLe mariage parfait pour FastAPI
Tortoise ORMActive Record async, inspiré DjangoPour FastAPI sans SQLAlchemy
Django ORMActive RecordInclus dans Django, très productif
OutilStyleNote
Eloquent (Laravel)Active RecordProductivité maximale
Doctrine (Symfony)Data MapperStyle Java/Hibernate, très puissant
Si tu…Choisis
Démarres en Node + Postgres avec équipe productivePrisma 7
Vises l’edge (Cloudflare Workers) ou veux du contrôle SQLDrizzle
Es en FastAPISQLAlchemy 2 async ou SQLModel
Es en DjangoDjango ORM (pas de choix)
Es en LaravelEloquent (pas de choix)
Es en SymfonyDoctrine ORM

C’est THE bug ORM le plus répandu. Il fait passer une page de 50 ms à 5 secondes.

// ❌ N+1 PROBLEM
const orders = await prisma.order.findMany({ take: 100 });
for (const order of orders) {
console.log(order.customer.name); // 100 requêtes SQL supplémentaires !
}
// Total : 1 + 100 = 101 requêtes
// ✅ Eager loading — 1 seule requête (avec JOIN)
const orders = await prisma.order.findMany({
take: 100,
include: { customer: true },
});
for (const order of orders) {
console.log(order.customer.name); // pas de requête supplémentaire
}
// Total : 1 requête

Toujours activer les logs SQL en dev :

// Prisma
const prisma = new PrismaClient({ log: ['query'] });

Tu vois exactement les requêtes générées. Si tu vois la même requête se répéter 50 fois → N+1.

Outils :

  • Prisma Studio + log
  • Laravel Debugbar (PHP)
  • Django Debug Toolbar (Python)
  • PgHero (PostgreSQL — analyse les slow queries)
Fenêtre de terminal
npm install prisma @prisma/client
npx prisma init
schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Customer {
id Int @id @default(autoincrement())
email String @unique
name String
orders Order[]
createdAt DateTime @default(now())
}
model Order {
id Int @id @default(autoincrement())
customer Customer @relation(fields: [customerId], references: [id])
customerId Int
status OrderStatus @default(pending)
totalCents Int @default(0)
createdAt DateTime @default(now())
@@index([customerId, createdAt])
}
enum OrderStatus {
pending
paid
shipped
cancelled
}
Fenêtre de terminal
npx prisma migrate dev --name init
npx prisma generate
// Usage
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Lecture
const orders = await prisma.order.findMany({
where: { customerId: 42, status: 'paid' },
include: { customer: true },
orderBy: { createdAt: 'desc' },
take: 20,
});
// Écriture
const order = await prisma.order.create({
data: {
customerId: 42,
totalCents: 2990,
status: 'pending',
},
});
// Transaction
await prisma.$transaction(async (tx) => {
await tx.order.update({ where: { id: 1 }, data: { status: 'paid' } });
await tx.product.update({ where: { id: 5 }, data: { stock: { decrement: 1 } } });
});
// Raw SQL quand nécessaire
const result = await prisma.$queryRaw<{ total: number }[]>`
SELECT SUM(total_cents) AS total FROM orders WHERE status = 'paid'
`;

Depuis Prisma 7 (2026), l’engine Rust est remplacé par un engine TypeScript + WebAssembly. Bénéfices :

  • Bundle ~85 % plus léger (1.6 Mo vs 11 Mo).
  • Performances ~3× meilleures sur grandes requêtes.
  • Compatible edge (Cloudflare Workers, Vercel Edge).
  • Plus de processus séparé à gérer.

C’est le saut générationnel qui rend Prisma viable sur edge runtime.

Fenêtre de terminal
npm install drizzle-orm postgres
npm install -D drizzle-kit
schema.ts
import { pgTable, serial, text, integer, timestamp, pgEnum, uniqueIndex, index } from 'drizzle-orm/pg-core';
import { sql, relations } from 'drizzle-orm';
export const orderStatus = pgEnum('order_status', ['pending', 'paid', 'shipped', 'cancelled']);
export const customers = pgTable('customers', {
id: serial('id').primaryKey(),
email: text('email').notNull(),
name: text('name').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => ({
emailIdx: uniqueIndex('customers_email_idx').on(t.email),
}));
export const orders = pgTable('orders', {
id: serial('id').primaryKey(),
customerId: integer('customer_id').notNull().references(() => customers.id),
status: orderStatus('status').notNull().default('pending'),
totalCents: integer('total_cents').notNull().default(0),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => ({
customerCreatedIdx: index('orders_customer_created_idx').on(t.customerId, t.createdAt),
}));
export const customersRelations = relations(customers, ({ many }) => ({
orders: many(orders),
}));
export const ordersRelations = relations(orders, ({ one }) => ({
customer: one(customers, {
fields: [orders.customerId],
references: [customers.id],
}),
}));
// Code
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { eq, and, desc } from 'drizzle-orm';
const client = postgres(process.env.DATABASE_URL!);
const db = drizzle(client, { schema });
const recentOrders = await db.query.orders.findMany({
where: and(
eq(orders.customerId, 42),
eq(orders.status, 'paid')
),
with: { customer: true }, // eager loading
orderBy: desc(orders.createdAt),
limit: 20,
});
Fenêtre de terminal
npx drizzle-kit generate # génère le SQL diff
npx drizzle-kit migrate # applique

Insérer 10 000 lignes une par une = 10 000 requêtes = catastrophe. Toujours bulk :

// Prisma
await prisma.product.createMany({ data: arrayOf10kProducts });
// Drizzle
await db.insert(products).values(arrayOf10kProducts);
// SQL brut
INSERT INTO products (sku, name, price) VALUES
('A', 'Foo', 100),
('B', 'Bar', 200),
('C', 'Baz', 300),
...;

Postgres permet aussi COPY pour des imports massifs (millions de lignes en quelques secondes).

Cas où le SQL natif est meilleur que l’ORM :

  1. Reporting complexe : window functions, CTE imbriquées, agrégats.
  2. Performances critiques : éviter les abstractions.
  3. Features spécifiques DB : LISTEN/NOTIFY, pg_advisory_lock, etc.
  4. Migration de données : INSERT ... SELECT ... ON CONFLICT.
  5. Recherche full-text avancée : tsvector, tsquery.
// Prisma raw SQL (typé)
const result = await prisma.$queryRaw<{ month: Date; total: number }[]>`
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(total_cents) AS total
FROM orders
WHERE created_at >= NOW() - INTERVAL '12 months'
GROUP BY 1
ORDER BY 1;
`;

$queryRaw (Prisma) ou db.execute(sql\…`)` (Drizzle) paramétrise automatiquement les valeurs interpolées — pas de SQL injection.

Migrations — ne JAMAIS éditer une migration appliquée

Section intitulée « Migrations — ne JAMAIS éditer une migration appliquée »

Une migration appliquée en prod est immuable. Si tu as besoin de corriger, crée une nouvelle migration.

Sinon les environnements dérivent (dev a une version, staging une autre, prod encore une autre) et plus rien ne marche.

Tu loades 100 commandes puis affiches le nom du client de chacune. Sans précaution, tu vois 101 requêtes SQL. Comment fixer ?
Tu insères 50 000 produits depuis un fichier CSV avec un boucle for { prisma.product.create(...) }. Que se passe-t-il ?
Tu modifies une migration déjà appliquée en prod (tu changes son SQL). Conséquence ?

Suite : 9.4 — Données opérationnelles — sauvegardes, réplication, sharding.