1. Introduction
Qu'est-ce que Prisma ? Prisma est un ORM/ODM de nouvelle génération qui simplifie l'accès aux bases de données en proposant une approche déclarative de la modélisation des données et une API type-safe pour interagir avec elles.
Prisma se compose de trois outils principaux :
- Prisma Schema : Un langage de définition de schéma (SDL) déclaratif pour modéliser vos données
- Prisma Client : Un client de base de données auto-généré et type-safe basé sur votre schéma
- Prisma CLI : Un outil en ligne de commande pour initialiser, générer et migrer votre base de données
Cet ORM moderne pour Node.js et TypeScript se distingue par sa simplicité d'utilisation, ses fonctionnalités avancées et sa capacité à travailler avec différents types de bases de données (relationnelles comme MySQL et non-relationnelles comme MongoDB). Il permet aux développeurs de définir leurs modèles de données dans un schéma déclaratif, et génère ensuite un client type-safe qui offre une API intuitive pour interagir avec la base de données.
Dans ce chapitre, nous explorerons comment intégrer Prisma dans une application Express, en couvrant à la fois son utilisation avec MySQL (ORM) et avec MongoDB (ODM). Nous aborderons la configuration, la modélisation des données, les opérations CRUD (Create, Read, Update, Delete), les relations entre entités, et les bonnes pratiques pour optimiser les performances de votre application.
2. Présentation de Prisma
2.1. Avantages de Prisma
- Type-safety : Détection des erreurs à la compilation plutôt qu'à l'exécution
- Auto-complétion dans les éditeurs de code comme VS Code
- Requêtes intuitives avec une API fluide
- Relations simplifiées entre les entités
- Migrations automatisées de la base de données
- Support multi-bases de données (MySQL, PostgreSQL, SQLite, SQL Server et MongoDB)
- Performance optimisée grâce à un pooling de connexions intelligent
2.2. Installation de Prisma
Pour commencer avec Prisma dans un projet Express, installez les dépendances nécessaires :
1# Initialiser un projet Node.js si ce n'est pas déjà fait 2npm init -y 3 4# Installer Express 5npm install express 6 7# Installer Prisma comme dépendance de développement 8npm install prisma --save-dev 9 10# Installer le client Prisma comme dépendance de production 11npm install @prisma/client
Initialisez ensuite Prisma dans votre projet :
1npx prisma init
Cette commande crée un dossier prisma
avec un fichier schema.prisma
initial et un fichier .env
pour stocker vos variables d'environnement, notamment l'URL de connexion à votre base de données.
3. Utilisation de Prisma avec MySQL (ORM)
3.1. Configuration pour MySQL
Pour configurer Prisma avec MySQL, modifiez le fichier .env
pour définir l'URL de connexion :
1DATABASE_URL="mysql://utilisateur:motdepasse@localhost:3306/nom_base_de_donnees"
Ensuite, dans le fichier schema.prisma
, spécifiez le fournisseur de base de données :
1generator client { 2 provider = "prisma-client-js" 3} 4 5datasource db { 6 provider = "mysql" 7 url = env("DATABASE_URL") 8}
3.2. Définition des modèles
Définissez vos modèles dans le fichier schema.prisma
en utilisant la syntaxe de Prisma Schema :
1model User { 2 id Int @id @default(autoincrement()) 3 email String @unique 4 name String? 5 password String 6 createdAt DateTime @default(now()) @map("created_at") 7 updatedAt DateTime @updatedAt @map("updated_at") 8 posts Post[] 9 10 @@map("users") 11} 12 13model Post { 14 id Int @id @default(autoincrement()) 15 title String 16 content String? 17 published Boolean @default(false) 18 createdAt DateTime @default(now()) @map("created_at") 19 updatedAt DateTime @updatedAt @map("updated_at") 20 authorId Int @map("author_id") 21 author User @relation(fields: [authorId], references: [id]) 22 23 @@map("posts") 24}
Dans cet exemple :
- Nous définissons deux modèles :
User
etPost
- Nous spécifions les champs, leurs types et les attributs (comme
@id
,@unique
,@default
) - Nous établissons une relation un-à-plusieurs entre
User
etPost
(un utilisateur peut avoir plusieurs posts) - Nous utilisons
@@map
pour définir le nom des tables dans la base de données
3.3. Génération du client et migration
Après avoir défini vos modèles, générez le client Prisma et effectuez la migration de la base de données :
1# Générer le client Prisma (Optionnel) voir ↓ 2npx prisma generate 3 4# Créer une migration et l'appliquer 5npx prisma migrate dev --name init
La commande migrate dev
effectue plusieurs actions :
- Sauvegarde vos migrations dans le dossier
prisma/migrations
- Applique les migrations à votre base de données
- Régénère le client Prisma
3.4. Intégration avec Express
Voici comment intégrer Prisma dans une application Express pour effectuer des opérations CRUD :
1const express = require('express'); 2const { PrismaClient } = require('@prisma/client'); 3 4const app = express(); 5const prisma = new PrismaClient(); 6 7app.use(express.json()); 8 9// Middleware pour gérer les erreurs Prisma 10function handlePrismaError(error, res) { 11 console.error('Erreur de base de données:', error); 12 13 if (error.code === 'P2002') { 14 return res.status(409).json({ 15 error: 'Une entrée avec cette valeur existe déjà' 16 }); 17 } 18 19 if (error.code === 'P2025') { 20 return res.status(404).json({ 21 error: 'Ressource non trouvée' 22 }); 23 } 24 25 return res.status(500).json({ 26 error: 'Erreur interne du serveur' 27 }); 28} 29 30// Créer un utilisateur 31app.post('/users', async (req, res) => { 32 const { email, name, password } = req.body; 33 34 try { 35 const user = await prisma.user.create({ 36 data: { 37 email, 38 name, 39 password, // Dans une vraie application, hashez le mot de passe! 40 }, 41 }); 42 43 res.status(201).json(user); 44 } catch (error) { 45 handlePrismaError(error, res); 46 } 47}); 48 49// Récupérer tous les utilisateurs 50app.get('/users', async (req, res) => { 51 try { 52 const users = await prisma.user.findMany({ 53 select: { 54 id: true, 55 email: true, 56 name: true, 57 createdAt: true, 58 // Ne pas sélectionner le mot de passe pour des raisons de sécurité 59 }, 60 }); 61 62 res.json(users); 63 } catch (error) { 64 handlePrismaError(error, res); 65 } 66}); 67 68// Récupérer un utilisateur par ID 69app.get('/users/:id', async (req, res) => { 70 const { id } = req.params; 71 72 try { 73 const user = await prisma.user.findUnique({ 74 where: { 75 id: parseInt(id), 76 }, 77 select: { 78 id: true, 79 email: true, 80 name: true, 81 createdAt: true, 82 posts: true, // Inclure les posts de l'utilisateur 83 }, 84 }); 85 86 if (!user) { 87 return res.status(404).json({ error: 'Utilisateur non trouvé' }); 88 } 89 90 res.json(user); 91 } catch (error) { 92 handlePrismaError(error, res); 93 } 94}); 95 96// Mettre à jour un utilisateur 97app.put('/users/:id', async (req, res) => { 98 const { id } = req.params; 99 const { email, name } = req.body; 100 101 try { 102 const user = await prisma.user.update({ 103 where: { 104 id: parseInt(id), 105 }, 106 data: { 107 email, 108 name, 109 }, 110 }); 111 112 res.json(user); 113 } catch (error) { 114 handlePrismaError(error, res); 115 } 116}); 117 118// Supprimer un utilisateur 119app.delete('/users/:id', async (req, res) => { 120 const { id } = req.params; 121 122 try { 123 await prisma.user.delete({ 124 where: { 125 id: parseInt(id), 126 }, 127 }); 128 129 res.status(204).end(); 130 } catch (error) { 131 handlePrismaError(error, res); 132 } 133}); 134 135// Routes pour les posts 136app.post('/posts', async (req, res) => { 137 const { title, content, published, authorId } = req.body; 138 139 try { 140 const post = await prisma.post.create({ 141 data: { 142 title, 143 content, 144 published: published || false, 145 author: { 146 connect: { id: parseInt(authorId) }, 147 }, 148 }, 149 }); 150 151 res.status(201).json(post); 152 } catch (error) { 153 handlePrismaError(error, res); 154 } 155}); 156 157app.get('/posts', async (req, res) => { 158 try { 159 const posts = await prisma.post.findMany({ 160 include: { 161 author: { 162 select: { 163 id: true, 164 name: true, 165 email: true, 166 }, 167 }, 168 }, 169 }); 170 171 res.json(posts); 172 } catch (error) { 173 handlePrismaError(error, res); 174 } 175}); 176 177// Démarrer le serveur 178const PORT = process.env.PORT || 3000; 179app.listen(PORT, () => { 180 console.log(`Serveur en écoute sur le port ${PORT}`); 181}); 182 183// Gérer la fermeture propre de la connexion Prisma 184process.on('SIGINT', async () => { 185 await prisma.$disconnect(); 186 process.exit(); 187});
3.5. Opérations avancées avec Prisma et MySQL
Requêtes avec filtres et tri
1// Récupérer les posts publiés, triés par date de création décroissante 2app.get('/posts/published', async (req, res) => { 3 try { 4 const posts = await prisma.post.findMany({ 5 where: { 6 published: true, 7 }, 8 orderBy: { 9 createdAt: 'desc', 10 }, 11 include: { 12 author: { 13 select: { 14 name: true, 15 }, 16 }, 17 }, 18 }); 19 20 res.json(posts); 21 } catch (error) { 22 handlePrismaError(error, res); 23 } 24});
Pagination
1app.get('/posts/paginated', async (req, res) => { 2 const { page = 1, limit = 10 } = req.query; 3 const skip = (parseInt(page) - 1) * parseInt(limit); 4 5 try { 6 const [posts, total] = await Promise.all([ 7 prisma.post.findMany({ 8 skip, 9 take: parseInt(limit), 10 orderBy: { 11 createdAt: 'desc', 12 }, 13 }), 14 prisma.post.count(), 15 ]); 16 17 res.json({ 18 posts, 19 meta: { 20 total, 21 page: parseInt(page), 22 limit: parseInt(limit), 23 pages: Math.ceil(total / parseInt(limit)), 24 }, 25 }); 26 } catch (error) { 27 handlePrismaError(error, res); 28 } 29});
Transactions
Les transactions sont essentielles pour maintenir l'intégrité des données lors d'opérations interdépendantes :
1app.post('/users/:id/posts', async (req, res) => { 2 const { id } = req.params; 3 const { title, content } = req.body; 4 5 try { 6 // Utiliser une transaction pour s'assurer que les deux opérations réussissent ou échouent ensemble 7 const result = await prisma.$transaction(async (tx) => { 8 // Vérifier que l'utilisateur existe 9 const user = await tx.user.findUnique({ 10 where: { id: parseInt(id) }, 11 }); 12 13 if (!user) { 14 throw new Error('Utilisateur non trouvé'); 15 } 16 17 // Créer le post 18 const post = await tx.post.create({ 19 data: { 20 title, 21 content, 22 author: { 23 connect: { id: parseInt(id) }, 24 }, 25 }, 26 }); 27 28 // Mettre à jour le compteur de posts de l'utilisateur (exemple hypothétique: le champ postCount n'existe pas) 29 await tx.user.update({ 30 where: { id: parseInt(id) }, 31 data: { 32 postCount: { increment: 1 }, 33 }, 34 }); 35 36 return post; 37 }); 38 39 res.status(201).json(result); 40 } catch (error) { 41 console.error('Erreur de transaction:', error); 42 res.status(400).json({ error: error.message }); 43 } 44});
4. Utilisation de Prisma avec MongoDB (ODM)
4.1. Configuration pour MongoDB
Pour utiliser Prisma avec MongoDB, modifiez votre fichier .env
:
DATABASE_URL="mongodb+srv://utilisateur:motdepasse@cluster0.mongodb.net/nom_base_de_donnees?retryWrites=true&w=majority"
Et mettez à jour le fichier schema.prisma
:
1generator client { 2 provider = "prisma-client-js" 3} 4 5datasource db { 6 provider = "mongodb" 7 url = env("DATABASE_URL") 8}
4.2. Définition des modèles pour MongoDB
Les modèles pour MongoDB sont similaires à ceux pour les bases de données relationnelles, mais avec quelques différences importantes, notamment l'utilisation d'identifiants sous forme de chaînes de caractères (ObjectId) :
1model User { 2 id String @id @default(auto()) @map("_id") @db.ObjectId 3 email String @unique 4 name String? 5 password String 6 createdAt DateTime @default(now()) 7 updatedAt DateTime @updatedAt 8 posts Post[] 9} 10 11model Post { 12 id String @id @default(auto()) @map("_id") @db.ObjectId 13 title String 14 content String? 15 published Boolean @default(false) 16 createdAt DateTime @default(now()) 17 updatedAt DateTime @updatedAt 18 authorId String @db.ObjectId 19 author User @relation(fields: [authorId], references: [id]) 20 tags String[] 21}
Notez les différences par rapport au modèle MySQL :
- Utilisation de
@db.ObjectId
pour les identifiants MongoDB - Mapping de
_id
avec@map("_id")
pour suivre la convention MongoDB - Ajout d'un champ
tags
de type array, spécifique à MongoDB
4.3. Synchronisation du schéma
Avec MongoDB, au lieu d'utiliser des migrations, vous pouvez utiliser db push
pour synchroniser votre schéma :
1npx prisma db push
Cette commande synchronise votre schéma avec la base de données MongoDB sans créer de fichiers de migration.
4.4. Intégration avec Express pour MongoDB
L'intégration avec Express est très similaire à celle pour MySQL, avec quelques adaptations pour les spécificités de MongoDB :
1const express = require('express'); 2const { PrismaClient } = require('@prisma/client'); 3 4const app = express(); 5const prisma = new PrismaClient(); 6 7app.use(express.json()); 8 9// Créer un utilisateur 10app.post('/users', async (req, res) => { 11 const { email, name, password } = req.body; 12 13 try { 14 const user = await prisma.user.create({ 15 data: { 16 email, 17 name, 18 password, // À hasher dans une vraie application 19 }, 20 }); 21 22 res.status(201).json(user); 23 } catch (error) { 24 console.error(error); 25 res.status(500).json({ error: 'Erreur lors de la création de l\'utilisateur' }); 26 } 27}); 28 29// Récupérer un utilisateur avec ses posts 30app.get('/users/:id', async (req, res) => { 31 const { id } = req.params; 32 33 try { 34 const user = await prisma.user.findUnique({ 35 where: { 36 id, 37 }, 38 include: { 39 posts: true, 40 }, 41 }); 42 43 if (!user) { 44 return res.status(404).json({ error: 'Utilisateur non trouvé' }); 45 } 46 47 res.json(user); 48 } catch (error) { 49 console.error(error); 50 res.status(500).json({ error: 'Erreur lors de la récupération de l\'utilisateur' }); 51 } 52}); 53 54// Créer un post avec des tags (spécifique à MongoDB) 55app.post('/posts', async (req, res) => { 56 const { title, content, published, authorId, tags } = req.body; 57 58 try { 59 const post = await prisma.post.create({ 60 data: { 61 title, 62 content, 63 published: published || false, 64 author: { 65 connect: { id: authorId }, 66 }, 67 tags: tags || [], 68 }, 69 }); 70 71 res.status(201).json(post); 72 } catch (error) { 73 console.error(error); 74 res.status(500).json({ error: 'Erreur lors de la création du post' }); 75 } 76}); 77 78// Rechercher des posts par tag (spécifique à MongoDB) 79app.get('/posts/tags/:tag', async (req, res) => { 80 const { tag } = req.params; 81 82 try { 83 const posts = await prisma.post.findMany({ 84 where: { 85 tags: { 86 has: tag, 87 }, 88 }, 89 include: { 90 author: { 91 select: { 92 name: true, 93 }, 94 }, 95 }, 96 }); 97 98 res.json(posts); 99 } catch (error) { 100 console.error(error); 101 res.status(500).json({ error: 'Erreur lors de la recherche des posts' }); 102 } 103}); 104 105// Démarrer le serveur 106const PORT = process.env.PORT || 3000; 107app.listen(PORT, () => { 108 console.log(`Serveur en écoute sur le port ${PORT}`); 109});
4.5. Opérations avancées avec Prisma et MongoDB
Recherche texte
MongoDB offre des capacités de recherche texte que vous pouvez exploiter via Prisma :
1app.get('/posts/search', async (req, res) => { 2 const { query } = req.query; 3 4 if (!query) { 5 return res.status(400).json({ error: 'Un terme de recherche est requis' }); 6 } 7 8 try { 9 // Recherche dans les titres et contenus 10 const posts = await prisma.post.findMany({ 11 where: { 12 OR: [ 13 { title: { contains: query, mode: 'insensitive' } }, 14 { content: { contains: query, mode: 'insensitive' } }, 15 ], 16 }, 17 include: { 18 author: { 19 select: { 20 name: true, 21 }, 22 }, 23 }, 24 }); 25 26 res.json(posts); 27 } catch (error) { 28 console.error(error); 29 res.status(500).json({ error: 'Erreur lors de la recherche' }); 30 } 31});
Agrégations
Bien que Prisma ne prenne pas directement en charge toutes les opérations d'agrégation de MongoDB, vous pouvez utiliser $queryRaw
pour des requêtes complexes :
1app.get('/posts/stats', async (req, res) => { 2 try { 3 // Utiliser $queryRaw pour exécuter une agrégation MongoDB 4 const stats = await prisma.$queryRaw` 5 db.Post.aggregate([ 6 { 7 $group: { 8 _id: "$authorId", 9 count: { $sum: 1 }, 10 averageContentLength: { $avg: { $strLenCP: "$content" } } 11 } 12 }, 13 { 14 $lookup: { 15 from: "User", 16 localField: "_id", 17 foreignField: "_id", 18 as: "author" 19 } 20 }, 21 { 22 $unwind: "$author" 23 }, 24 { 25 $project: { 26 _id: 0, 27 authorId: "$_id", 28 authorName: "$author.name", 29 postCount: "$count", 30 averageContentLength: 1 31 } 32 } 33 ] 34 `; 35 36 res.json(stats); 37 } catch (error) { 38 console.error(error); 39 res.status(500).json({ error: 'Erreur lors de l\'obtention des statistiques' }); 40 } 41});
5. Bonnes pratiques avec Prisma
5.1. Gestion des instances Prisma
Il est recommandé de créer une seule instance de PrismaClient
pour toute votre application :
1// db.js 2const { PrismaClient } = require('@prisma/client'); 3 4const prisma = new PrismaClient(); 5 6module.exports = prisma;
1// userRoutes.js 2const express = require('express'); 3const router = express.Router(); 4const prisma = require('./db'); 5 6router.get('/users', async (req, res) => { 7 const users = await prisma.user.findMany(); 8 res.json(users); 9}); 10 11module.exports = router;
5.2. Middleware Prisma
Prisma permet de définir des middlewares pour intercepter et modifier les requêtes :
1// Middleware pour le logging 2prisma.$use(async (params, next) => { 3 const before = Date.now(); 4 5 const result = await next(params); 6 7 const after = Date.now(); 8 console.log(`Requête ${params.model}.${params.action} a pris ${after - before}ms`); 9 10 return result; 11}); 12 13// Middleware pour la soft deletion 14prisma.$use(async (params, next) => { 15 if (params.model === 'Post') { 16 if (params.action === 'delete') { 17 // Convertir delete en update 18 params.action = 'update'; 19 params.args.data = { deleted: true }; 20 } 21 if (params.action === 'findUnique' || params.action === 'findMany') { 22 // Ajouter le filtre deleted: false 23 params.args.where = { 24 ...params.args.where, 25 deleted: false, 26 }; 27 } 28 } 29 30 return next(params); 31});
5.3. Structuration du code avec les repositories
Pour les applications de grande taille, il est recommandé d'organiser votre code en utilisant le pattern Repository :
1// repositories/userRepository.js 2const prisma = require('../db'); 3 4class UserRepository { 5 async findAll(options = {}) { 6 const { skip, take, where, include, orderBy } = options; 7 8 return prisma.user.findMany({ 9 skip, 10 take, 11 where, 12 include, 13 orderBy, 14 }); 15 } 16 17 async findById(id, include = {}) { 18 return prisma.user.findUnique({ 19 where: { id }, 20 include, 21 }); 22 } 23 24 async create(data) { 25 return prisma.user.create({ 26 data, 27 }); 28 } 29 30 async update(id, data) { 31 return prisma.user.update({ 32 where: { id }, 33 data, 34 }); 35 } 36 37 async delete(id) { 38 return prisma.user.delete({ 39 where: { id }, 40 }); 41 } 42} 43 44module.exports = new UserRepository();
1// controllers/userController.js 2const userRepository = require('../repositories/userRepository'); 3 4exports.getAllUsers = async (req, res) => { 5 try { 6 const users = await userRepository.findAll(); 7 res.json(users); 8 } catch (error) { 9 res.status(500).json({ error: error.message }); 10 } 11}; 12 13exports.getUserById = async (req, res) => { 14 try { 15 const user = await userRepository.findById(req.params.id, { posts: true }); 16 17 if (!user) { 18 return res.status(404).json({ error: 'Utilisateur non trouvé' }); 19 } 20 21 res.json(user); 22 } catch (error) { 23 res.status(500).json({ error: error.message }); 24 } 25}; 26 27// Autres méthodes de contrôleur...
5.4. Performance et optimisation
Sélection des champs
Sélectionnez uniquement les champs dont vous avez besoin pour réduire la taille des données transférées :
1const users = await prisma.user.findMany({ 2 select: { 3 id: true, 4 name: true, 5 email: true, 6 // Ne pas inclure le mot de passe et autres champs sensibles 7 }, 8});
Chargement différé vs chargement anticipé
Choisissez judicieusement entre le chargement anticipé (eager loading) et le chargement différé (lazy loading) :
1// Chargement anticipé: récupérer l'utilisateur avec ses posts en une seule requête 2const userWithPosts = await prisma.user.findUnique({ 3 where: { id }, 4 include: { 5 posts: true, 6 }, 7}); 8 9// Chargement différé: récupérer les posts séparément si nécessaire 10const user = await prisma.user.findUnique({ 11 where: { id }, 12}); 13 14if (needPosts) { 15 const posts = await prisma.post.findMany({ 16 where: { 17 authorId: user.id, 18 }, 19 }); 20}
Pagination pour les grandes collections
Toujours utiliser la pagination pour les grandes collections de données :
1const { page = 1, limit = 10 } = req.query; 2const skip = (parseInt(page) - 1) * parseInt(limit); 3 4const users = await prisma.user.findMany({ 5 skip, 6 take: parseInt(limit), 7 orderBy: { 8 createdAt: 'desc', 9 }, 10}); 11 12const total = await prisma.user.count();
6. Conclusion
Prisma offre une solution élégante et puissante pour interagir avec les bases de données relationnelles (comme MySQL) et non relationnelles (comme MongoDB) dans les applications Express. Grâce à son approche déclarative pour la définition des schémas et son API type-safe, il réduit considérablement la complexité du code et minimise les risques d'erreurs à l'exécution.
Dans ce chapitre, nous avons exploré les concepts fondamentaux de Prisma, sa configuration avec MySQL et MongoDB, les opérations CRUD de base, ainsi que des fonctionnalités plus avancées comme les transactions, les relations, et les optimisations de performance. Nous avons également abordé des bonnes pratiques pour organiser votre code dans des applications plus complexes.
L'intégration de Prisma dans vos applications Express vous permettra de développer plus rapidement, avec moins d'erreurs, et d'écrire un code plus maintenable sur le long terme. Que vous travailliez avec des bases de données relationnelles traditionnelles ou des bases de données NoSQL modernes, Prisma vous offre une interface unifiée et intuitive pour gérer vos données.
7. Exercices pratiques
Exercice 1 : API REST avec Prisma et MySQL
Créez une API REST complète pour gérer une bibliothèque avec les entités suivantes :
- Livres (titre, année de publication, ISBN, description)
- Auteurs (nom, biographie, date de naissance)
- Catégories (nom, description)
Établissez les relations appropriées entre ces entités (un livre peut avoir plusieurs auteurs, un livre appartient à une ou plusieurs catégories) et implémentez des routes pour :
- Ajouter/modifier/supprimer des livres, auteurs et catégories
- Lister tous les livres avec leurs auteurs et catégories
- Rechercher des livres par titre, auteur ou catégorie
- Trier les livres par année de publication ou par titre
Exercice 2 : API de blog avec Prisma et MongoDB
Développez une API pour un système de blog avec les fonctionnalités suivantes :
- Gestion des utilisateurs (inscription, connexion, profil)
- Publication d'articles avec support des tags
- Système de commentaires (avec commentaires imbriqués)
- Fonctionnalité de recherche texte
- Système de likes/