Requêtes générées : prédire puis vérifier
Prédire le SQL, éviter N+1, choisir le bon niveau.
Objectifs
À la fin de cette leçon, vous saurez :
- utiliser le repository CRUD et prédire le SQL avant chaque appel ;
- charger des relations (relations, join) sans tomber dans N+1 ;
- savoir quand descendre au query builder ou au SQL brut.
🔗 Pour vous rafraîchir la mémoire : Pool pg et requêtes paramétrées ($1, $2) · CRUD en SQL · jointures SQL
Le réflexe pédagogique : prédire AVANT
Avec logging: true, chaque appel affiche son SQL. Le jeu : écrire la requête attendue sur papier, exécuter, comparer.
const repo = dataSource.getRepository(Task);
// Prédiction : SELECT * FROM task
await repo.find();
// Prédiction : SELECT * FROM task WHERE fait = false ORDER BY id ASC LIMIT 20
await repo.find({
where: { fait: false },
order: { id: "ASC" },
take: 20,
});
// Prédiction : INSERT INTO task (titre, fait) VALUES ($1, $2) RETURNING ...
await repo.save({ titre: "Prédiction", fait: false });
Si votre prédiction rate, c'est que l'abstraction a gagné sur vous : relisez la leçon SQL correspondante. L'ORM est un accélérateur pour qui connaît déjà le trajet.
Les relations et le piège N+1
// Version 1 : lazy par défaut → N+1 !
const tasks = await repo.find();
for (const t of tasks) {
console.log(t.user.email); // une requête PAR tâche !
}
// Version 2 : eager via join — UNE requête
const tasks = await repo.find({ relations: { user: true } });
// SELECT task.* JOIN user ON ...
Le problème N+1 : 1 requête pour la liste + 1 par élément pour sa relation = 101 requêtes pour 100 tâches. Symptôme classique d'un ORM utilisé sans comprendre ce qu'il génère. Diagnostic : le log ; correctif : relations (join) ou chargement explicite.
Sélections ciblées
// Tout ramener est du gaspillage :
await repo.find({
select: { id: true, titre: true },
relations: { user: { email: true } }, // seulement l'email du user
});
Quand descendre d'un niveau
// Agrégat complexe : le query builder reste lisible
const stats = await repo
.createQueryBuilder("task")
.select("user.nom", "nom")
.addSelect("COUNT(task.id)", "total")
.innerJoin("task.user", "user")
.groupBy("user.nom")
.getRawMany();
Et parfois, rien ne remplace le SQL brut paramétré :
const rows = await dataSource.query(
"SELECT titre FROM tasks WHERE titre ILIKE $1",
["%urgent%"]
);
Grille de décision :
CRUD simple → repository
jointures/agrégats modérés → query builder
fenêtres, CTE, optimisations → SQL brut paramétré
Exercice
- Pour chacun des cinq appels ci-dessus : prédisez, exécutez, comparez.
- Provoquez un N+1 avec 50 tâches, comptez les requêtes au log, corrigez.
- Écrivez en query builder « nombre de tâches faites par utilisateur ».
- Quel appel repository produirait
DELETE FROM task WHERE id = $1?
Résumé
- Repository = CRUD objet ; toujours prédire le SQL avant.
- relations/join contre N+1 ; select ciblé par hygiène.
- Query builder puis SQL brut quand la complexité monte — jamais l'inverse.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Question 4. repo.delete(id) — ou repo.remove(entiteChargée) si vous avez déjà l'objet (remove fait alors un DELETE identique après un SELECT préalable). Deux API, deux coûts : delete évite le chargement.