Skip to main content

6. ORM & Prisma

Pour le moment, nos requêtes HTTP ne permettent pas de persistence de données. Cela signifie que si nous redémarrons notre serveur, nous perdons toutes les données. Pour éviter cela, nous allons devoir utiliser une base de données.

Intégrer SQL

Voyons comment interagir avec une base de données en utilisant du SQL brut.

Installation d'un client SQL

Pour SQLite, nous pouvons utiliser le package better-sqlite3 :

npm install better-sqlite3
npm install --save-dev @types/better-sqlite3

Connexion et création de table

import Database from 'better-sqlite3'

// Connexion à la base de données
const db = new Database('./database.db')

// Création d'une table utilisateurs
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`)

Requêtes SQL brutes

Fichier user.route.ts :

import {Router} from 'express'
import Database from 'better-sqlite3'

export const userRouter = Router()

const db = new Database('./database.db')

// GET: Récupérer tous les utilisateurs
// Accessible via GET /users
userRouter.get('/', (_req, res) => {
const users = db.prepare('SELECT * FROM users').all()
res.json(users)
})

// GET: Récupérer un utilisateur par ID
// Accessible via GET /users/:id
userRouter.get('/:id', (req, res) => {
const {id} = req.params
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(id)

if (!user) {
return res.status(404).json({error: 'Utilisateur non trouvé'})
}

res.json(user)
})

// POST: Créer un utilisateur
// Accessible via POST /users
userRouter.post('/', (req, res) => {
const {name, email} = req.body

try {
const result = db
.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
.run(name, email)
res.status(201).json({
message: 'Utilisateur créé',
id: result.lastInsertRowid,
name,
email,
})
} catch (error: any) {
res.status(400).json({error: error.message})
}
})

Les problèmes du SQL brut

Comme vous pouvez le voir, écrire du SQL brut présente plusieurs inconvénients :

  1. Verbosité : Chaque requête nécessite d'écrire du SQL manuellement
  2. Pas de typage : TypeScript ne peut pas vérifier si vos requêtes sont correctes
  3. Gestion d'erreurs complexe : Il faut gérer manuellement chaque cas d'erreur SQL
  4. Migrations manuelles : Modifier la structure de la base nécessite de réécrire les requêtes
  5. Risque de sécurité : Même avec les paramètres préparés (?), il faut être vigilant contre les injections SQL
  6. Code difficile à maintenir : Les requêtes complexes deviennent rapidement illisibles dans les gros projets
  7. Portabilité limitée : Changer de SGBD (MySQL → PostgreSQL) nécessite de réécrire les requêtes

C'est là qu'interviennent les ORM pour simplifier tout cela et garantir la sécurité et la maintenabilité du code.

ORM

Les ORM (Object-Relational Mapping) sont des outils permettant de gérer les interactions entre une base de données relationnelle et le code d'une application. Ils simplifient les requêtes et la gestion des données en traduisant des objets dans votre code en enregistrements dans la base de données, et vice-versa.

Dans un environnement Node.js, les ORM jouent un rôle crucial en permettant de :

  • Simplifier les requêtes SQL complexes
  • Gérer les migrations de manière fluide
  • Éviter l'écriture de SQL brut, ce qui améliore la maintenabilité du code
  • Accéder aux bases de données de manière déclarative et sécurisée

Pourquoi utiliser un ORM ?

L'utilisation d'un ORM présente plusieurs avantages importants :

  • Sécurité renforcée : L'ORM réduit les risques d'injections SQL en générant automatiquement des requêtes sécurisées
  • Abstraction des bases de données : Vous pouvez interagir avec la base sans connaître en détail le SQL, rendant le code plus lisible et maintenable
  • Gestion des migrations : L'ORM facilite les évolutions de la base de données, crucial dans les environnements agiles
  • Performance optimisée : Les ORM modernes comme Prisma génèrent des requêtes SQL performantes
  • Typage automatique : Avec TypeScript, les ORMs offrent une sécurité de type et de l'autocomplétion

Prisma

Prisma est un ORM (Object-Relational Mapping) moderne qui simplifie la gestion des bases de données dans les projets Node.js. Contrairement aux ORM traditionnels, Prisma adopte une approche déclarative qui facilite la synchronisation entre votre code et votre base de données.

Prisma offre de nombreux avantages :

  • Facilité d'utilisation : Écriture simple et intuitive des requêtes grâce au Prisma Client.
  • Type safety : Prisma génère automatiquement des types TypeScript basés sur votre schéma, réduisant les erreurs.
  • Automatisation des migrations : Gestion des changements dans votre base de données avec un minimum d'effort.
  • Performances optimisées : Prisma utilise des requêtes SQL optimisées.

Concepts de base

Prisma repose sur trois composants principaux :

  • prisma/schema.prisma : Un fichier .prisma qui définit le modèle de données, les relations, et les configurations de la base de données.
  • Prisma Client : Un client généré automatiquement pour interagir avec la base de données à travers votre code.
  • Prisma CLI : Un outil en ligne de commande pour gérer le schéma, les migrations, et bien plus.

Schéma simplifié d'illustration Prisma

Structure requête HTTP

Installation et initialisation

Pour ajouter Prisma à votre projet, vous pouvez utiliser :

npm install prisma --save-dev
npm install @prisma/client

Ensuite, initialisez Prisma dans votre projet :

npx prisma init

Cette commande crée deux fichiers principaux :

  • prisma/schema.prisma : Le fichier principal pour définir votre modèle.
  • .env : Un fichier de configuration pour les variables d'environnement, comme l'URL de votre base de données.

Connecteurs

Prisma prend en charge plusieurs connecteurs de base de données, tels que MySQL, PostgreSQL, SQLite, et SQL Server.

datasource db {
provider = "sqlite"
}

Le fichier de config de prisma prisma.config.ts doit aussi être mis à jour pour utiliser le bon connecteur.

import "dotenv/config";
import {defineConfig, env} from "prisma/config";

export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: env("DATABASE_URL"),
},
});

DATABASE_URL="file:./dev.db"

Migration

Une migration Prisma correspond à un ensemble de modifications appliquées à une base de données pour aligner sa structure (tables, colonnes, relations, etc.) avec le modèle de données défini dans le fichier schema.prisma. Ce processus est utilisé pour synchroniser le modèle Prisma avec la base de données, particulièrement dans les projets où le modèle évolue fréquemment.

  • Créer un schéma : Dans le fichier schema.prisma vous pouvez définir un modèle correspondant à une table.
model User {
id Int @id @default(autoincrement())
name String
email String @unique
}

Ce modèle définit une table users avec trois colonnes : id (clé primaire auto-incrémentée), name (texte obligatoire) et email (texte unique).

  • Créer une migration :

    npx prisma migrate dev --name init

Cette commande :

  • Génère un fichier de migration contenant les instructions SQL nécessaires.
  • Applique les modifications à votre base de données.

Generate

La commande generate sert à recréer le Prisma Client lorsque votre schéma change. Sans cette commande, les modifications dans schema.prisma ne seront pas reflétées dans votre code.

  • Générer le client :

    npx prisma generate
  • Quand l'utiliser ? : Après toute modification du fichier schema.prisma ou lors de l'installation de nouvelles dépendances Prisma.

Seed

Le seeding consiste à insérer des données initiales dans votre base, par exemple pour des tests ou un environnement de développement.

  • Configurer le script de seed : Dans prisma.config.ts, ajoutez :
import "dotenv/config";
import {defineConfig, env} from "prisma/config";

export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "tsx prisma/seed.ts",
},
datasource: {
url: env("DATABASE_URL"),
},
});
  • Créer le script de seed : Exemple de fichier prisma/seed.ts :
import {PrismaClient} from '../src/generated/prisma/index.js'
import {PrismaBetterSqlite3} from '@prisma/adapter-better-sqlite3'

const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || 'file:./dev.db',
})
const prisma = new PrismaClient({adapter})

async function main() {
// Suppression de tous les utilisateurs
await prisma.user.deleteMany()

// Réinitialisation de l'auto-incrémentation (spécifique à SQLite)
await prisma.$executeRaw`DELETE
FROM sqlite_sequence
WHERE name = 'User'`

// Création de plusieurs utilisateurs avec createMany
await prisma.user.createMany({
data: [
{
name: 'Alice',
email: 'alice@example.com',
},
{
name: 'Bob',
email: 'bob@example.com',
},
{
name: 'John Doe',
email: 'john@example.com',
},
],
})

console.log('Base de données peuplée avec succès !')
}

main()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})

  • Exécuter le seed :
    npx prisma db seed

La commande executeRaw dans ce cas est nécessaire sur SQLite pour réinitialiser l'auto-incrémentation des ID après la suppression des données.

Mise en pratique

Utiliser le client Prisma

Maintenant que nous avons configuré Prisma et peuplé notre base de données, voyons comment utiliser le Prisma Client pour créer les mêmes endpoints que nous avions avec SQL brut, mais de manière plus simple et sécurisée.

Pattern singleton

Pour éviter de créer une nouvelle instance du Prisma Client à chaque requête, il est recommandé d'utiliser un pattern singleton pour garantir une seule instance partagée entre les requêtes. Créons cela dans un fichier ./src/client.ts :

import {PrismaBetterSqlite3} from '@prisma/adapter-better-sqlite3'
import {PrismaClient} from "@/generated/prisma/client";

const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || 'file:./dev.db',
})

const prisma = new PrismaClient({adapter})

export default prisma

Mise à jour de l'api

Reprenons les mêmes endpoints que nous avions créés avec SQL brut, mais cette fois avec Prisma.

Fichier user.route.ts :

import {Router, Request, Response} from 'express'
import {prisma} from './client' // Import du client singleton

export const userRouter = Router()

// GET: Récupérer tous les utilisateurs
// Accessible via GET /users
userRouter.get('/', async (_req: Request, res: Response) => {
const users = await prisma.user.findMany()
res.status(200).json(users)
})

// GET: Récupérer un utilisateur par ID
// Accessible via GET /users/:id
userRouter.get('/:id', async (req: Request, res: Response) => {
const {id} = req.params
const user = await prisma.user.findUnique({
where: {id: parseInt(id)},
})

if (!user) {
return res.status(404).json({error: 'Utilisateur non trouvé'})
}

res.status(200).json(user)
})

// POST: Créer un utilisateur
// Accessible via POST /users
userRouter.post('/', async (req: Request, res: Response) => {
const {name, email} = req.body

try {
const user = await prisma.user.create({
data: {name, email},
})

res.status(201).json({
message: 'Utilisateur créé',
...user,
})
} catch (error: any) {
res.status(400).json({error: error.message})
}
})

Prisma studio

Prisma Studio est un outil de visualisation de données qui vous permet d'explorer et de modifier les données de votre base de données. Pour lancer Prisma Studio, exécutez :

npx prisma studio

Scripts

Pour faciliter votre workflow avec Prisma, vous pouvez ajouter des scripts npm dans votre package.json. Voici quelques scripts utiles qui vous feront gagner du temps :

{
"scripts": {
"db:migrate": "prisma migrate dev",
"db:generate": "prisma generate",
"db:seed": "prisma db seed",
"db:studio": "prisma studio",
"db:push": "prisma db push",
"db:reset": "prisma migrate reset",
"db:setup": "prisma generate && prisma migrate dev && prisma db seed"
}
}

Description des scripts :

  • db:migrate : Crée et applique une nouvelle migration en développement
  • db:generate : Régénère le Prisma Client après modification du schéma
  • db:seed : Exécute le script de seed pour peupler la base de données
  • db:studio : Lance Prisma Studio pour explorer vos données
  • db:push : Synchronise le schéma avec la base sans créer de migration (prototypage rapide)
  • db:reset : Réinitialise complètement la base de données et applique toutes les migrations
  • db:setup : Script complet pour configurer la base de données (generate + migrate + seed)

Astuce : Le script db:setup est particulièrement utile pour l'onboarding de nouveaux développeurs, car il configure automatiquement toute la base de données en une seule commande.

Commandes utiles

Prisma Client - Opérations de lecture

  • findMany : Récupère plusieurs enregistrements avec possibilité de filtrage, tri, et pagination.
  • findUnique : Récupère un enregistrement unique basé sur un identifiant ou champ unique.
  • findFirst : Récupère le premier enregistrement correspondant aux critères.

Prisma Client - Opérations d'écriture

  • create : Crée un nouvel enregistrement dans la base de données.
  • createMany : Crée plusieurs enregistrements en une seule opération.
  • update : Met à jour un enregistrement existant.
  • updateMany : Met à jour plusieurs enregistrements correspondant aux critères.
  • upsert : Met à jour un enregistrement s'il existe, sinon le crée.
  • delete : Supprime un enregistrement.
  • deleteMany : Supprime plusieurs enregistrements correspondant aux critères.

Prisma CLI - Commandes essentielles

  • prisma init : Initialise Prisma dans un projet existant.
  • prisma migrate dev : Crée et applique les migrations en développement.
  • prisma migrate deploy : Applique les migrations en production.
  • prisma db seed : Exécute le script de seed pour peupler la base de données.
  • prisma generate : Régénère le Prisma Client après modification du schéma.
  • prisma studio : Lance l'interface graphique Prisma Studio pour explorer et éditer les données.
  • prisma db push : Synchronise le schéma avec la base sans créer de migration (utile en développement rapide).

Les relations

Dans cet exemple, nous avons utilisé un modèle simple avec uniquement des utilisateurs. Cependant, Prisma excelle particulièrement dans la gestion des relations entre les différentes entités de votre base de données.

#$## Exemple de relations

Imaginons que nous voulions ajouter des posts liés aux utilisateurs :

model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[] // Un utilisateur peut avoir plusieurs posts
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
author User @relation(fields: [authorId], references: [id])
authorId Int // Clé étrangère vers User
}

Requêtes avec relations

Avec ce schéma, vous pouvez facilement récupérer un utilisateur avec tous ses posts :

// Récupérer un utilisateur avec tous ses posts
const userWithPosts = await prisma.user.findUnique({
where: {id: 1},
include: {posts: true}, // Inclut les posts liés
})

// Créer un utilisateur avec un post en même temps
const userWithPost = await prisma.user.create({
data: {
name: 'Charlie',
email: 'charlie@example.com',
posts: {
create: [
{
title: 'Mon premier post',
content: 'Contenu du post',
},
],
},
},
include: {posts: true}, // Retourne l'utilisateur avec ses posts
})

Pour en savoir plus : Consultez la documentation officielle sur les différents types de relations :