Skip to content

Latest commit

 

History

History
1744 lines (1366 loc) · 42.1 KB

File metadata and controls

1744 lines (1366 loc) · 42.1 KB

🔝 Retour au Sommaire

20.1.2. Node.js : node-postgres (pg), Prisma

Introduction

Node.js est une plateforme JavaScript côté serveur extrêmement populaire pour construire des APIs et applications web. Pour interagir avec PostgreSQL depuis Node.js, vous avez principalement deux approches :

  1. node-postgres (pg) : Driver natif bas niveau, contrôle total sur SQL
  2. Prisma : ORM moderne avec type-safety et génération de code

Ce tutoriel couvre les deux approches en profondeur pour vous permettre de choisir celle qui convient le mieux à votre projet.


Vue d'Ensemble : Driver vs ORM

Qu'est-ce qu'un Driver ?

Un driver (ou adaptateur) est une bibliothèque qui permet à votre code Node.js de communiquer directement avec PostgreSQL. Vous écrivez du SQL brut et récupérez les résultats.

Analogie : C'est comme parler directement au serveur PostgreSQL dans sa langue natale (SQL).

Qu'est-ce qu'un ORM ?

Un ORM (Object-Relational Mapping) est une couche d'abstraction qui traduit votre code JavaScript/TypeScript en requêtes SQL. Vous manipulez des objets JavaScript au lieu d'écrire du SQL.

Analogie : C'est comme avoir un traducteur automatique entre votre code et PostgreSQL.

Comparaison Rapide

Caractéristique node-postgres (pg) Prisma
Type Driver natif ORM moderne
Langage SQL brut API JavaScript/TypeScript
Courbe d'apprentissage Requiert connaissance SQL Plus facile pour débutants
Contrôle Total Abstraction (moins de contrôle)
Type-safety Non (sans effort) Oui (TypeScript natif)
Migrations Manuelles Intégrées
Performances Excellentes (direct SQL) Très bonnes (optimisé)
Complexité SQL Toutes requêtes possibles Limité aux capacités de l'ORM
Taille bundle Légère (~100 KB) Plus lourde (~2 MB)

Recommandation :

  • node-postgres : Pour les projets nécessitant du SQL complexe, microservices légers, ou équipes expérimentées en SQL
  • Prisma : Pour les projets TypeScript, équipes préférant la productivité, ou applications CRUD standard

Partie 1 : node-postgres (pg)

Introduction à node-postgres

node-postgres (npm package : pg) est le driver PostgreSQL le plus populaire pour Node.js. Il est :

  • Léger : Peu de dépendances
  • Mature : Utilisé en production depuis plus de 10 ans
  • Flexible : Support de toutes les fonctionnalités PostgreSQL
  • Performant : Accès direct sans overhead

Installation

npm install pg

Pour TypeScript (fortement recommandé) :

npm install --save-dev @types/node @types/pg

Connexion Basique

Connexion simple (client unique)

const { Client } = require('pg');

// Configuration de connexion
const client = new Client({
  host: 'localhost',
  port: 5432,
  database: 'ma_database',
  user: 'mon_utilisateur',
  password: 'mon_password',
});

// Connexion asynchrone
async function connect() {
  try {
    await client.connect();
    console.log('✅ Connecté à PostgreSQL');

    // Votre code ici

  } catch (error) {
    console.error('❌ Erreur de connexion:', error);
  } finally {
    await client.end(); // Toujours fermer la connexion
  }
}

connect();

Important : Client crée une connexion unique. Pour les applications web, utilisez plutôt Pool (voir ci-dessous).

Configuration avec variables d'environnement (recommandé)

Créez un fichier .env :

DATABASE_URL=postgresql://user:password@localhost:5432/mydb

Puis utilisez dotenv :

npm install dotenv
require('dotenv').config();  
const { Client } = require('pg');  

const client = new Client({
  connectionString: process.env.DATABASE_URL,
});

Pool de Connexions (Production)

En production, TOUJOURS utiliser un pool de connexions pour de meilleures performances :

const { Pool } = require('pg');

// Créer un pool global
const pool = new Pool({
  host: 'localhost',
  port: 5432,
  database: 'ma_database',
  user: 'mon_utilisateur',
  password: 'mon_password',
  max: 20,                    // Maximum 20 connexions
  idleTimeoutMillis: 30000,   // Fermer après 30s d'inactivité
  connectionTimeoutMillis: 2000, // Timeout de connexion 2s
});

// Utilisation du pool
async function queryDatabase() {
  try {
    const result = await pool.query('SELECT NOW()');
    console.log(result.rows[0]);
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Fermer le pool à la fin de l'application
process.on('SIGTERM', async () => {
  await pool.end();
});

Avantages du Pool :

  • Réutilisation des connexions (plus rapide)
  • Gestion automatique des connexions
  • Idéal pour serveurs web (Express, Fastify, etc.)

Exécution de Requêtes

SELECT : Récupérer des données

const { Pool } = require('pg');  
const pool = new Pool(/* config */);  

async function getUsers() {
  try {
    // Requête simple
    const result = await pool.query('SELECT * FROM utilisateurs');

    console.log(`Nombre de lignes : ${result.rowCount}`);
    console.log('Colonnes :', result.fields.map(f => f.name));
    console.log('Données :', result.rows);

    // Parcourir les résultats
    result.rows.forEach(user => {
      console.log(`ID: ${user.id}, Nom: ${user.nom}, Email: ${user.email}`);
    });

    return result.rows;
  } catch (error) {
    console.error('Erreur SELECT:', error);
    throw error;
  }
}

getUsers();

Structure du résultat :

{
  rows: [
    { id: 1, nom: 'Alice', email: 'alice@example.com' },
    { id: 2, nom: 'Bob', email: 'bob@example.com' }
  ],
  rowCount: 2,
  fields: [/* métadonnées des colonnes */],
  command: 'SELECT'
}

Requêtes Paramétrées (Sécurité Critique)

⚠️ JAMAIS comme ceci (VULNÉRABLE) :

// ❌ DANGEREUX - Injection SQL possible !
const email = "alice@example.com' OR '1'='1";  
const query = `SELECT * FROM users WHERE email = '${email}'`;  
await pool.query(query);  

✅ TOUJOURS utiliser des paramètres :

// ✅ SÉCURISÉ - Paramètres échappés automatiquement
const email = 'alice@example.com';  
const query = 'SELECT * FROM users WHERE email = $1';  
const result = await pool.query(query, [email]);  

Syntaxe des paramètres :

  • $1, $2, $3, etc. : Positions des paramètres
  • Les valeurs sont passées dans un tableau : [param1, param2, ...]

Exemples avec plusieurs paramètres :

// Paramètres multiples
const query = 'SELECT * FROM produits WHERE categorie = $1 AND prix > $2';  
const result = await pool.query(query, ['Électronique', 100]);  

// INSERT avec paramètres
const insertQuery = 'INSERT INTO users (nom, email, age) VALUES ($1, $2, $3) RETURNING id';  
const values = ['Charlie', 'charlie@example.com', 35];  
const result = await pool.query(insertQuery, values);  
console.log('Nouvel ID:', result.rows[0].id);  

Opérations CRUD Complètes

CREATE : Insérer des données

async function createUser(nom, email, age) {
  const query = `
    INSERT INTO utilisateurs (nom, email, age)
    VALUES ($1, $2, $3)
    RETURNING id, nom, email, created_at
  `;

  try {
    const result = await pool.query(query, [nom, email, age]);
    const newUser = result.rows[0];
    console.log('✅ Utilisateur créé:', newUser);
    return newUser;
  } catch (error) {
    // Violation de contrainte UNIQUE (email déjà existant)
    if (error.code === '23505') {
      throw new Error('Cet email existe déjà');
    }
    throw error;
  }
}

// Utilisation
createUser('Alice', 'alice@example.com', 30);

Insertion multiple (bulk insert) :

async function createMultipleUsers(users) {
  // users = [['Alice', 'alice@...', 30], ['Bob', 'bob@...', 25]]

  const query = `
    INSERT INTO utilisateurs (nom, email, age)
    VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9)
  `;

  // Aplatir le tableau
  const values = users.flat();

  const result = await pool.query(query, values);
  console.log(`${result.rowCount} utilisateurs créés`);
}

Meilleure approche pour bulk insert :

const format = require('pg-format'); // npm install pg-format

async function bulkInsertUsers(users) {
  // users = [['Alice', 'alice@...', 30], ['Bob', 'bob@...', 25]]

  const query = format(
    'INSERT INTO utilisateurs (nom, email, age) VALUES %L RETURNING id',
    users
  );

  const result = await pool.query(query);
  return result.rows.map(r => r.id);
}

READ : Lire des données

// Récupérer un utilisateur par ID
async function getUserById(id) {
  const query = 'SELECT * FROM utilisateurs WHERE id = $1';
  const result = await pool.query(query, [id]);

  if (result.rowCount === 0) {
    return null; // Utilisateur non trouvé
  }

  return result.rows[0];
}

// Récupérer avec filtres multiples
async function searchUsers(filters) {
  const { nom, ageMin, ageMax } = filters;

  const query = `
    SELECT * FROM utilisateurs
    WHERE nom ILIKE $1
      AND age BETWEEN $2 AND $3
    ORDER BY nom
  `;

  const result = await pool.query(query, [`%${nom}%`, ageMin, ageMax]);
  return result.rows;
}

// Utilisation
const users = await searchUsers({ nom: 'Ali', ageMin: 20, ageMax: 40 });

UPDATE : Modifier des données

async function updateUser(id, updates) {
  const { nom, email, age } = updates;

  const query = `
    UPDATE utilisateurs
    SET nom = $1, email = $2, age = $3, updated_at = NOW()
    WHERE id = $4
    RETURNING *
  `;

  const result = await pool.query(query, [nom, email, age, id]);

  if (result.rowCount === 0) {
    throw new Error('Utilisateur non trouvé');
  }

  return result.rows[0];
}

// Utilisation
const updated = await updateUser(1, {
  nom: 'Alice Smith',
  email: 'alice.smith@example.com',
  age: 31
});

UPDATE partiel (uniquement les champs fournis) :

async function partialUpdateUser(id, updates) {
  const fields = [];
  const values = [];
  let paramCounter = 1;

  // Construire dynamiquement la requête
  Object.keys(updates).forEach(key => {
    fields.push(`${key} = $${paramCounter}`);
    values.push(updates[key]);
    paramCounter++;
  });

  if (fields.length === 0) {
    throw new Error('Aucun champ à mettre à jour');
  }

  const query = `
    UPDATE utilisateurs
    SET ${fields.join(', ')}, updated_at = NOW()
    WHERE id = $${paramCounter}
    RETURNING *
  `;

  values.push(id);

  const result = await pool.query(query, values);
  return result.rows[0];
}

// Utilisation : met à jour uniquement l'âge
await partialUpdateUser(1, { age: 32 });

DELETE : Supprimer des données

async function deleteUser(id) {
  const query = 'DELETE FROM utilisateurs WHERE id = $1 RETURNING *';

  const result = await pool.query(query, [id]);

  if (result.rowCount === 0) {
    throw new Error('Utilisateur non trouvé');
  }

  return result.rows[0];
}

// Suppression multiple
async function deleteUsersByAge(ageMax) {
  const query = 'DELETE FROM utilisateurs WHERE age < $1';
  const result = await pool.query(query, [ageMax]);
  console.log(`${result.rowCount} utilisateurs supprimés`);
}

Gestion des Transactions

Transaction basique

async function transferMoney(fromAccountId, toAccountId, amount) {
  // Récupérer une connexion du pool
  const client = await pool.connect();

  try {
    // Démarrer la transaction
    await client.query('BEGIN');

    // Opération 1 : Débiter le compte source
    await client.query(
      'UPDATE comptes SET solde = solde - $1 WHERE id = $2',
      [amount, fromAccountId]
    );

    // Opération 2 : Créditer le compte destination
    await client.query(
      'UPDATE comptes SET solde = solde + $1 WHERE id = $2',
      [amount, toAccountId]
    );

    // Valider la transaction
    await client.query('COMMIT');
    console.log('✅ Transfert réussi');

  } catch (error) {
    // Annuler la transaction en cas d'erreur
    await client.query('ROLLBACK');
    console.error('❌ Transfert annulé:', error);
    throw error;
  } finally {
    // Toujours libérer la connexion
    client.release();
  }
}

Points clés :

  • await pool.connect() : Obtenir une connexion dédiée
  • BEGIN : Démarrer une transaction
  • COMMIT : Valider les changements
  • ROLLBACK : Annuler les changements
  • client.release() : Rendre la connexion au pool

Transaction avec SAVEPOINT

async function complexTransaction() {
  const client = await pool.connect();

  try {
    await client.query('BEGIN');

    // Opération 1
    await client.query('INSERT INTO logs (message) VALUES ($1)', ['Op 1']);

    // Créer un point de sauvegarde
    await client.query('SAVEPOINT mon_savepoint');

    try {
      // Opération 2 (risquée)
      await client.query('INSERT INTO risky_table (data) VALUES ($1)', ['data']);
    } catch (error) {
      // Revenir au savepoint (annule uniquement l'Op 2)
      await client.query('ROLLBACK TO SAVEPOINT mon_savepoint');
      console.log('Savepoint restauré, Op 1 toujours valide');
    }

    // Opération 3
    await client.query('INSERT INTO logs (message) VALUES ($1)', ['Op 3']);

    await client.query('COMMIT');
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

Types de Données PostgreSQL

Types numériques

// INTEGER, BIGINT
await pool.query('INSERT INTO produits (id, quantite) VALUES ($1, $2)', [1, 100]);

// NUMERIC/DECIMAL (utiliser des strings pour la précision)
await pool.query('INSERT INTO prix (montant) VALUES ($1)', ['19.99']);

// FLOAT/DOUBLE PRECISION
await pool.query('INSERT INTO mesures (valeur) VALUES ($1)', [3.14159]);

Types texte

// VARCHAR, TEXT
await pool.query('INSERT INTO articles (titre, contenu) VALUES ($1, $2)',
  ['Mon Titre', 'Contenu très long...']
);

// CHAR (taille fixe)
await pool.query('INSERT INTO codes (code_postal) VALUES ($1)', ['75001']);

Types temporels

// DATE
const date = new Date('2025-11-23');  
await pool.query('INSERT INTO evenements (date) VALUES ($1)', [date]);  

// TIMESTAMP / TIMESTAMPTZ
const now = new Date();  
await pool.query('INSERT INTO logs (timestamp) VALUES ($1)', [now]);  

// INTERVAL
await pool.query("SELECT NOW() + INTERVAL '7 days'");

Lecture des dates :

const result = await pool.query('SELECT created_at FROM users WHERE id = $1', [1]);  
const date = result.rows[0].created_at; // Objet Date JavaScript  
console.log(date.toISOString()); // "2025-11-23T10:30:00.000Z"  

JSON et JSONB

// Insertion JSON
const profile = { nom: 'Alice', age: 30, tags: ['developer', 'postgresql'] };

await pool.query('INSERT INTO users (profile) VALUES ($1)', [JSON.stringify(profile)]);
// Ou directement (pg le sérialise automatiquement)
await pool.query('INSERT INTO users (profile) VALUES ($1)', [profile]);

// Lecture JSON
const result = await pool.query('SELECT profile FROM users WHERE id = $1', [1]);  
const userProfile = result.rows[0].profile; // Objet JavaScript déjà parsé  
console.log(userProfile.nom); // 'Alice'  

Requêtes JSONB avancées :

// Recherche dans JSONB
const result = await pool.query(
  "SELECT * FROM users WHERE profile->>'nom' = $1",
  ['Alice']
);

// Extraction de chemin
const result = await pool.query(
  "SELECT profile->'tags'->0 as premier_tag FROM users"
);

Arrays (Tableaux PostgreSQL)

// Insertion de tableau
const tags = ['javascript', 'postgresql', 'nodejs'];  
await pool.query('INSERT INTO articles (tags) VALUES ($1)', [tags]);  

// Lecture de tableau
const result = await pool.query('SELECT tags FROM articles WHERE id = $1', [1]);  
const articleTags = result.rows[0].tags; // ['javascript', 'postgresql', 'nodejs']  

// Recherche dans tableau
const result = await pool.query(
  "SELECT * FROM articles WHERE $1 = ANY(tags)",
  ['postgresql']
);

UUID

const { v4: uuidv4 } = require('uuid'); // npm install uuid

// Génération côté application (UUID v4)
const id = uuidv4();  
await pool.query('INSERT INTO sessions (id, token) VALUES ($1, $2)', [id, 'token123']);  

// Génération côté PostgreSQL (UUID v7 dans PG 18)
await pool.query('INSERT INTO events (id, data) VALUES (gen_uuid_v7(), $1)', ['event data']);

// Lecture UUID
const result = await pool.query('SELECT id FROM sessions WHERE token = $1', ['token123']);  
const sessionId = result.rows[0].id; // String UUID  

Prepared Statements (Optimisation)

Les prepared statements permettent de préparer une requête une fois et de l'exécuter plusieurs fois avec différents paramètres.

async function usePreparedStatement() {
  const client = await pool.connect();

  try {
    // Préparer la requête
    await client.query({
      name: 'get-user-by-email',
      text: 'SELECT * FROM users WHERE email = $1'
    });

    // Exécuter plusieurs fois (plus rapide)
    const result1 = await client.query('get-user-by-email', ['alice@example.com']);
    const result2 = await client.query('get-user-by-email', ['bob@example.com']);

    // PostgreSQL réutilise le plan d'exécution

  } finally {
    client.release();
  }
}

Gain de performance : ~10-30% pour les requêtes répétées.

Gestion des Erreurs

Codes d'erreur PostgreSQL courants

async function handleErrors() {
  try {
    await pool.query('INSERT INTO users (email) VALUES ($1)', ['duplicate@example.com']);
  } catch (error) {
    console.error('Code erreur:', error.code);
    console.error('Message:', error.message);
    console.error('Détail:', error.detail);

    // Codes SQLSTATE courants
    switch (error.code) {
      case '23505': // Unique violation
        console.error('Cette valeur existe déjà');
        break;
      case '23503': // Foreign key violation
        console.error('Référence introuvable');
        break;
      case '23502': // NOT NULL violation
        console.error('Champ obligatoire manquant');
        break;
      case '42P01': // Undefined table
        console.error('Table inexistante');
        break;
      case '42703': // Undefined column
        console.error('Colonne inexistante');
        break;
      default:
        console.error('Erreur PostgreSQL:', error.message);
    }
  }
}

Retry logic (reconnexion automatique)

async function queryWithRetry(query, params, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await pool.query(query, params);
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }

      // Réessayer uniquement pour les erreurs de connexion
      if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
        console.log(`Tentative ${attempt}/${maxRetries} échouée, retry...`);
        await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
      } else {
        throw error;
      }
    }
  }
}

Exemple Complet : API REST avec Express

const express = require('express');  
const { Pool } = require('pg');  

const app = express();  
app.use(express.json());  

// Configuration du pool
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
});

// Middleware de gestion d'erreurs
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// GET /users - Liste des utilisateurs
app.get('/users', asyncHandler(async (req, res) => {
  const result = await pool.query('SELECT id, nom, email FROM utilisateurs ORDER BY nom');
  res.json(result.rows);
}));

// GET /users/:id - Un utilisateur
app.get('/users/:id', asyncHandler(async (req, res) => {
  const { id } = req.params;
  const result = await pool.query('SELECT * FROM utilisateurs WHERE id = $1', [id]);

  if (result.rowCount === 0) {
    return res.status(404).json({ error: 'Utilisateur non trouvé' });
  }

  res.json(result.rows[0]);
}));

// POST /users - Créer un utilisateur
app.post('/users', asyncHandler(async (req, res) => {
  const { nom, email, age } = req.body;

  const result = await pool.query(
    'INSERT INTO utilisateurs (nom, email, age) VALUES ($1, $2, $3) RETURNING *',
    [nom, email, age]
  );

  res.status(201).json(result.rows[0]);
}));

// PUT /users/:id - Modifier un utilisateur
app.put('/users/:id', asyncHandler(async (req, res) => {
  const { id } = req.params;
  const { nom, email, age } = req.body;

  const result = await pool.query(
    'UPDATE utilisateurs SET nom = $1, email = $2, age = $3 WHERE id = $4 RETURNING *',
    [nom, email, age, id]
  );

  if (result.rowCount === 0) {
    return res.status(404).json({ error: 'Utilisateur non trouvé' });
  }

  res.json(result.rows[0]);
}));

// DELETE /users/:id - Supprimer un utilisateur
app.delete('/users/:id', asyncHandler(async (req, res) => {
  const { id } = req.params;

  const result = await pool.query('DELETE FROM utilisateurs WHERE id = $1 RETURNING *', [id]);

  if (result.rowCount === 0) {
    return res.status(404).json({ error: 'Utilisateur non trouvé' });
  }

  res.json({ message: 'Utilisateur supprimé', user: result.rows[0] });
}));

// Middleware de gestion d'erreurs global
app.use((error, req, res, next) => {
  console.error('Erreur serveur:', error);

  if (error.code === '23505') {
    return res.status(409).json({ error: 'Cette valeur existe déjà' });
  }

  res.status(500).json({ error: 'Erreur serveur interne' });
});

// Démarrage du serveur
const PORT = process.env.PORT || 3000;  
app.listen(PORT, () => {  
  console.log(`🚀 Serveur démarré sur le port ${PORT}`);
});

// Fermeture propre
process.on('SIGTERM', async () => {
  await pool.end();
  process.exit(0);
});

Partie 2 : Prisma (ORM Moderne)

Introduction à Prisma

Prisma est un ORM next-generation pour Node.js et TypeScript. Il offre :

  • Type-safety : Types TypeScript générés automatiquement
  • Migrations : Gestion intégrée du schéma de base de données
  • Prisma Studio : Interface graphique pour explorer vos données
  • Prisma Client : API auto-générée pour vos requêtes

Architecture Prisma

Votre Application (TypeScript/JavaScript)
           ↓
    Prisma Client (généré)
           ↓
    Prisma Query Engine
           ↓
    PostgreSQL Database

Fichiers clés :

  • schema.prisma : Définition du schéma (single source of truth)
  • prisma/migrations/ : Historique des migrations
  • node_modules/.prisma/client/ : Client généré automatiquement

Installation et Configuration

Installation

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

Initialisation

npx prisma init

Cette commande crée :

  • prisma/schema.prisma : Fichier de configuration
  • .env : Variables d'environnement

Configuration du schema.prisma

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Définition du modèle (table)
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  nom       String
  age       Int?     // ? = nullable
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relation
  posts     Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  authorId  Int

  // Relation many-to-one
  author    User     @relation(fields: [authorId], references: [id])
}

Fichier .env :

DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

Workflow Prisma

1. Définir le schéma (schema.prisma)

Vous définissez vos modèles dans schema.prisma.

2. Créer une migration

npx prisma migrate dev --name init

Cette commande :

  • Génère le SQL de migration
  • Applique la migration à la base de données
  • Régénère le Prisma Client

3. Générer le Client

npx prisma generate

Génère/régénère le Prisma Client avec vos types TypeScript.

4. Utiliser le Client dans votre code

const { PrismaClient } = require('@prisma/client');  
const prisma = new PrismaClient();  

// Votre code ici

CRUD avec Prisma

CREATE : Créer des enregistrements

const { PrismaClient } = require('@prisma/client');  
const prisma = new PrismaClient();  

async function main() {
  // Créer un utilisateur
  const user = await prisma.user.create({
    data: {
      email: 'alice@example.com',
      nom: 'Alice',
      age: 30,
    },
  });

  console.log('Utilisateur créé:', user);
}

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

Création avec relation :

// Créer un utilisateur ET ses posts en une seule requête
const userWithPosts = await prisma.user.create({
  data: {
    email: 'bob@example.com',
    nom: 'Bob',
    posts: {
      create: [
        { title: 'Mon premier post', content: 'Hello World!' },
        { title: 'Mon deuxième post', content: 'Prisma est génial' },
      ],
    },
  },
  include: {
    posts: true, // Inclure les posts dans le résultat
  },
});

Création multiple (bulk insert) :

const users = await prisma.user.createMany({
  data: [
    { email: 'user1@example.com', nom: 'User 1', age: 25 },
    { email: 'user2@example.com', nom: 'User 2', age: 30 },
    { email: 'user3@example.com', nom: 'User 3', age: 35 },
  ],
  skipDuplicates: true, // Ignorer les doublons (email unique)
});

console.log(`${users.count} utilisateurs créés`);

READ : Lire des données

// Récupérer tous les utilisateurs
const allUsers = await prisma.user.findMany();

// Récupérer avec filtres
const filteredUsers = await prisma.user.findMany({
  where: {
    age: {
      gte: 18, // Greater Than or Equal (age >= 18)
      lte: 65, // Less Than or Equal (age <= 65)
    },
  },
  orderBy: {
    nom: 'asc', // Tri ascendant
  },
  take: 10, // LIMIT 10
  skip: 0,  // OFFSET 0 (pagination)
});

// Récupérer un seul utilisateur par ID
const user = await prisma.user.findUnique({
  where: {
    id: 1,
  },
});

// Récupérer avec relations
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: true, // Charger les posts associés
  },
});

console.log('Utilisateur:', userWithPosts.nom);  
console.log('Nombre de posts:', userWithPosts.posts.length);  

Recherche avancée :

// Opérateurs de filtrage
const users = await prisma.user.findMany({
  where: {
    OR: [
      { email: { contains: '@gmail.com' } },  // LIKE '%@gmail.com%'
      { nom: { startsWith: 'A' } },            // LIKE 'A%'
    ],
    AND: [
      { age: { not: null } },
      { createdAt: { gte: new Date('2025-01-01') } },
    ],
  },
});

// Recherche full-text (si configuré)
const results = await prisma.user.findMany({
  where: {
    nom: {
      search: 'Alice Bob', // PostgreSQL full-text search
    },
  },
});

UPDATE : Modifier des données

// Mettre à jour un utilisateur
const updatedUser = await prisma.user.update({
  where: {
    id: 1,
  },
  data: {
    age: 31,
    email: 'newemail@example.com',
  },
});

// Mettre à jour plusieurs enregistrements
const updateResult = await prisma.user.updateMany({
  where: {
    age: {
      lt: 18, // Moins de 18 ans
    },
  },
  data: {
    age: 18, // Définir à 18
  },
});

console.log(`${updateResult.count} utilisateurs mis à jour`);

// Incrémenter une valeur
const incrementedUser = await prisma.user.update({
  where: { id: 1 },
  data: {
    age: {
      increment: 1, // age = age + 1
    },
  },
});

DELETE : Supprimer des données

// Supprimer un utilisateur
const deletedUser = await prisma.user.delete({
  where: {
    id: 1,
  },
});

// Supprimer plusieurs utilisateurs
const deleteResult = await prisma.user.deleteMany({
  where: {
    age: {
      lt: 18,
    },
  },
});

console.log(`${deleteResult.count} utilisateurs supprimés`);

// Supprimer tous les enregistrements (attention !)
await prisma.user.deleteMany({});

Relations dans Prisma

One-to-Many (Un à Plusieurs)

model User {
  id    Int    @id @default(autoincrement())
  nom   String
  posts Post[] // Un user a plusieurs posts
}

model Post {
  id       Int    @id @default(autoincrement())
  title    String
  authorId Int
  author   User   @relation(fields: [authorId], references: [id])
}

Utilisation :

// Créer un post pour un utilisateur existant
const post = await prisma.post.create({
  data: {
    title: 'Nouveau post',
    author: {
      connect: { id: 1 }, // Lier à l'utilisateur ID 1
    },
  },
});

// Récupérer un utilisateur avec ses posts
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true }, // Filtrer les posts
      orderBy: { createdAt: 'desc' },
    },
  },
});

Many-to-Many (Plusieurs à Plusieurs)

model Post {
  id         Int        @id @default(autoincrement())
  title      String
  categories Category[] @relation("PostCategories")
}

model Category {
  id    Int    @id @default(autoincrement())
  name  String
  posts Post[] @relation("PostCategories")
}

Prisma crée automatiquement la table de jonction _PostCategories.

Utilisation :

// Créer un post avec plusieurs catégories
const post = await prisma.post.create({
  data: {
    title: 'Mon article',
    categories: {
      connect: [
        { id: 1 },
        { id: 2 },
      ],
    },
  },
});

// Récupérer avec relations
const postWithCategories = await prisma.post.findUnique({
  where: { id: 1 },
  include: {
    categories: true,
  },
});

Transactions avec Prisma

Transaction basique

// Transaction automatique avec $transaction
const [user, post] = await prisma.$transaction([
  prisma.user.create({
    data: { email: 'alice@example.com', nom: 'Alice' },
  }),
  prisma.post.create({
    data: { title: 'Premier post', authorId: 1 },
  }),
]);

// Si une opération échoue, tout est annulé (rollback)

Transaction interactive

Pour des transactions complexes avec logique conditionnelle :

const result = await prisma.$transaction(async (tx) => {
  // tx est un Prisma Client transactionnel

  // Opération 1
  const user = await tx.user.create({
    data: { email: 'bob@example.com', nom: 'Bob' },
  });

  // Logique conditionnelle
  if (user.id % 2 === 0) {
    // Opération 2
    await tx.post.create({
      data: { title: 'Post pair', authorId: user.id },
    });
  }

  return user;
});

Requêtes Brutes (Raw SQL)

Parfois, vous avez besoin d'écrire du SQL brut pour des requêtes complexes :

// Requête brute avec typage
const users = await prisma.$queryRaw`
  SELECT * FROM "User"
  WHERE age > ${18}
  ORDER BY nom
`;

console.log(users);

// Requête brute sans typage
const result = await prisma.$queryRawUnsafe(
  'SELECT * FROM "User" WHERE email = $1',
  'alice@example.com'
);

// Exécution SQL (INSERT, UPDATE, DELETE)
const affected = await prisma.$executeRaw`
  UPDATE "User" SET age = age + 1 WHERE age < ${50}
`;

console.log(`${affected} lignes affectées`);

⚠️ Attention : Utilisez toujours les template literals (backticks) pour éviter les injections SQL.

Migrations

Créer une migration

npx prisma migrate dev --name add_user_profile

Appliquer les migrations en production

npx prisma migrate deploy

Réinitialiser la base de données (développement)

npx prisma migrate reset

Warning : Supprime toutes les données !

Voir l'état des migrations

npx prisma migrate status

Prisma Studio (Interface Graphique)

Prisma Studio est une interface web pour explorer et modifier vos données :

npx prisma studio

Ouvre une interface sur http://localhost:5555 où vous pouvez :

  • Voir toutes vos tables
  • Filtrer et rechercher
  • Créer, modifier, supprimer des enregistrements
  • Visualiser les relations

Exemple Complet : API REST avec Prisma

const express = require('express');  
const { PrismaClient } = require('@prisma/client');  

const app = express();  
app.use(express.json());  

const prisma = new PrismaClient();

// GET /users - Liste des utilisateurs
app.get('/users', async (req, res) => {
  try {
    const users = await prisma.user.findMany({
      include: {
        posts: {
          where: { published: true },
        },
      },
    });
    res.json(users);
  } catch (error) {
    res.status(500).json({ error: 'Erreur serveur' });
  }
});

// GET /users/:id - Un utilisateur
app.get('/users/:id', async (req, res) => {
  try {
    const user = await prisma.user.findUnique({
      where: { id: parseInt(req.params.id) },
      include: { posts: true },
    });

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

    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Erreur serveur' });
  }
});

// POST /users - Créer un utilisateur
app.post('/users', async (req, res) => {
  try {
    const { email, nom, age } = req.body;

    const user = await prisma.user.create({
      data: { email, nom, age },
    });

    res.status(201).json(user);
  } catch (error) {
    if (error.code === 'P2002') {
      return res.status(409).json({ error: 'Email déjà utilisé' });
    }
    res.status(500).json({ error: 'Erreur serveur' });
  }
});

// PUT /users/:id - Modifier un utilisateur
app.put('/users/:id', async (req, res) => {
  try {
    const { email, nom, age } = req.body;

    const user = await prisma.user.update({
      where: { id: parseInt(req.params.id) },
      data: { email, nom, age },
    });

    res.json(user);
  } catch (error) {
    if (error.code === 'P2025') {
      return res.status(404).json({ error: 'Utilisateur non trouvé' });
    }
    res.status(500).json({ error: 'Erreur serveur' });
  }
});

// DELETE /users/:id - Supprimer un utilisateur
app.delete('/users/:id', async (req, res) => {
  try {
    await prisma.user.delete({
      where: { id: parseInt(req.params.id) },
    });

    res.json({ message: 'Utilisateur supprimé' });
  } catch (error) {
    if (error.code === 'P2025') {
      return res.status(404).json({ error: 'Utilisateur non trouvé' });
    }
    res.status(500).json({ error: 'Erreur serveur' });
  }
});

// Démarrage
const PORT = process.env.PORT || 3000;  
app.listen(PORT, () => {  
  console.log(`🚀 Serveur démarré sur le port ${PORT}`);
});

// Fermeture propre
process.on('SIGTERM', async () => {
  await prisma.$disconnect();
  process.exit(0);
});

Codes d'Erreur Prisma

Code Description Action
P2002 Contrainte unique violée Retourner 409 Conflict
P2003 Contrainte FK violée Retourner 400 Bad Request
P2025 Enregistrement non trouvé Retourner 404 Not Found
P1001 Impossible de se connecter Vérifier la connexion DB
P1017 Serveur fermé la connexion Retry avec backoff

Comparaison : node-postgres vs Prisma

Quand utiliser node-postgres (pg)

Choisir pg si :

  • Vous avez besoin de requêtes SQL complexes (CTEs récursifs, window functions avancées)
  • Vous optimisez pour des performances maximales
  • Votre équipe est experte en SQL
  • Vous avez des contraintes de taille (bundles légers)
  • Vous travaillez sur des microservices simples
  • Vous voulez un contrôle total sur le SQL généré

Exemple de cas d'usage :

  • Système analytique avec requêtes complexes
  • API haute performance avec latence critique
  • Scripts de migration de données
  • Applications legacy avec beaucoup de SQL existant

Quand utiliser Prisma

Choisir Prisma si :

  • Vous développez en TypeScript et voulez du type-safety
  • Vous voulez de la productivité et moins de boilerplate
  • Votre équipe est moins expérimentée en SQL
  • Vous avez besoin de migrations automatiques
  • Vous voulez Prisma Studio pour explorer vos données
  • Vous construisez une application CRUD standard

Exemple de cas d'usage :

  • SaaS moderne avec API REST/GraphQL
  • Applications full-stack TypeScript (Next.js, Nest.js)
  • Prototypes et MVPs rapides
  • Équipes préférant l'abstraction

Comparaison de Performance

Benchmark indicatif (1000 requêtes SELECT simples) :

  • node-postgres : ~150ms
  • Prisma : ~180ms

Différence : ~20% plus lent pour Prisma, mais négligeable dans la plupart des applications.

Approche Hybride

Vous pouvez combiner les deux :

const { PrismaClient } = require('@prisma/client');  
const { Pool } = require('pg');  

const prisma = new PrismaClient();  
const pool = new Pool(/* config */);  

// Utiliser Prisma pour les opérations CRUD standard
const users = await prisma.user.findMany();

// Utiliser pg pour les requêtes complexes
const analytics = await pool.query(`
  WITH monthly_stats AS (
    SELECT
      DATE_TRUNC('month', created_at) as month,
      COUNT(*) as user_count
    FROM "User"
    GROUP BY month
  )
  SELECT * FROM monthly_stats
  ORDER BY month DESC
  LIMIT 12
`);

Bonnes Pratiques

1. Gestion de la connexion

node-postgres :

// ✅ Bon : Pool global
const pool = new Pool({ /* config */ });

// Export pour réutilisation
module.exports = { pool };

// ❌ Éviter : Créer un nouveau pool à chaque requête

Prisma :

// ✅ Bon : Instance globale unique
const { PrismaClient } = require('@prisma/client');

const prisma = global.prisma || new PrismaClient();

if (process.env.NODE_ENV !== 'production') {
  global.prisma = prisma;
}

module.exports = { prisma };

2. Variables d'environnement

// .env
DATABASE_URL=postgresql://user:pass@localhost:5432/db  
NODE_ENV=development  
PORT=3000  

// Utilisation
require('dotenv').config();

const config = {
  database: process.env.DATABASE_URL,
  port: process.env.PORT || 3000,
};

3. Logging et Debugging

node-postgres :

const pool = new Pool({
  /* config */
  log: (msg) => console.log('PostgreSQL:', msg),
});

Prisma :

const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error'],
});

// Écouter les événements de requête
prisma.$on('query', (e) => {
  console.log('Query: ' + e.query);
  console.log('Duration: ' + e.duration + 'ms');
});

4. Gestion des erreurs centralisée

// Express error handler
app.use((error, req, res, next) => {
  console.error('Error:', error);

  // Erreurs PostgreSQL (pg)
  if (error.code === '23505') {
    return res.status(409).json({ error: 'Duplicate entry' });
  }

  // Erreurs Prisma
  if (error.code === 'P2002') {
    return res.status(409).json({ error: 'Duplicate entry' });
  }

  res.status(500).json({ error: 'Internal server error' });
});

5. Tests

Avec node-postgres :

const { Pool } = require('pg');

describe('User Repository', () => {
  let pool;

  beforeAll(async () => {
    pool = new Pool({
      connectionString: process.env.TEST_DATABASE_URL,
    });
  });

  afterAll(async () => {
    await pool.end();
  });

  it('should create a user', async () => {
    const result = await pool.query(
      'INSERT INTO users (email) VALUES ($1) RETURNING *',
      ['test@example.com']
    );
    expect(result.rows[0].email).toBe('test@example.com');
  });
});

Avec Prisma :

const { PrismaClient } = require('@prisma/client');

describe('User Repository', () => {
  let prisma;

  beforeAll(async () => {
    prisma = new PrismaClient({
      datasources: {
        db: { url: process.env.TEST_DATABASE_URL },
      },
    });
  });

  afterAll(async () => {
    await prisma.$disconnect();
  });

  it('should create a user', async () => {
    const user = await prisma.user.create({
      data: { email: 'test@example.com', nom: 'Test' },
    });
    expect(user.email).toBe('test@example.com');
  });
});

Ressources et Documentation

node-postgres (pg)

Prisma

Tutoriels et Guides


Résumé des Points Clés

node-postgres (pg)

  • Driver natif léger et performant
  • ✅ Utilisez toujours un Pool en production
  • Paramètres ($1, $2) pour éviter les injections SQL
  • ✅ Gestion manuelle des transactions avec BEGIN/COMMIT/ROLLBACK
  • ✅ Idéal pour SQL complexe et performances critiques

Prisma

  • ORM moderne avec type-safety TypeScript
  • Schéma unique (schema.prisma) = source de vérité
  • Migrations automatiques et versionnées
  • Prisma Studio pour explorer vos données visuellement
  • ✅ API intuitive et productive pour CRUD
  • ✅ Idéal pour développement rapide et maintenabilité

Choix Final

Pas de mauvais choix ! Les deux approches sont excellentes. Choisissez en fonction de :

  • Votre expertise (SQL vs ORM)
  • Vos besoins (performance vs productivité)
  • Votre stack (JavaScript vs TypeScript)
  • La complexité de vos requêtes

Conseil : Pour un nouveau projet TypeScript, commencez avec Prisma. Si vous avez besoin de SQL complexe, utilisez prisma.$queryRaw ou combinez avec node-postgres.


Prochaine étape : Explorez les autres drivers (Java JDBC, Go pgx, .NET Npgsql) pour comparer les approches et maîtriser PostgreSQL dans tous les écosystèmes.

⏭️ Java : JDBC, HikariCP, R2DBC