Skip to content

Latest commit

 

History

History
2027 lines (1594 loc) · 59.2 KB

File metadata and controls

2027 lines (1594 loc) · 59.2 KB

🔝 Retour au Sommaire

20.1.3. Java : JDBC, HikariCP, R2DBC

Introduction

Java dispose de plusieurs approches pour interagir avec PostgreSQL, chacune adaptée à différents besoins et architectures :

  1. JDBC : API standard Java pour l'accès aux bases de données (synchrone/bloquant)
  2. HikariCP : Pool de connexions haute performance pour JDBC
  3. R2DBC : API réactive non-bloquante pour des applications asynchrones modernes

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


Vue d'Ensemble : Les Trois Approches

JDBC (Java Database Connectivity)

JDBC est l'API standard Java pour l'accès aux bases de données depuis 1997. C'est la fondation sur laquelle reposent la plupart des frameworks Java.

Caractéristiques :

  • API synchrone et bloquante
  • Standard Java (java.sql.*)
  • Compatible avec tous les SGBD via des drivers spécifiques
  • Mature et stable

Analogie : JDBC est comme le langage universel que tous les systèmes de bases de données comprennent en Java.

HikariCP (Connection Pool)

HikariCP est un pool de connexions ultra-performant qui optimise l'utilisation de JDBC en réutilisant les connexions au lieu d'en créer de nouvelles à chaque requête.

Caractéristiques :

  • Pool de connexions le plus rapide du marché Java
  • Utilisé par défaut dans Spring Boot
  • Configuration simple et intuitive
  • Monitoring intégré

Analogie : Si JDBC est comme appeler un taxi à chaque fois, HikariCP est comme avoir une flotte de taxis toujours disponibles.

R2DBC (Reactive Relational Database Connectivity)

R2DBC est une API moderne réactive pour les bases de données relationnelles, basée sur le modèle Reactive Streams.

Caractéristiques :

  • API asynchrone et non-bloquante
  • Idéal pour les architectures réactives (WebFlux, Reactor)
  • Meilleure utilisation des ressources (scalabilité)
  • Standard récent (2018+)

Analogie : R2DBC est comme passer des commandes asynchrones qui vous notifient quand elles sont prêtes, au lieu d'attendre à chaque fois.

Comparaison Rapide

Caractéristique JDBC JDBC + HikariCP R2DBC
Modèle Synchrone bloquant Synchrone bloquant Asynchrone non-bloquant
Maturité Très mature (1997) Très mature Récent (2018+)
Performance Moyenne Excellente Excellente
Complexité Simple Simple Moyenne (réactif)
Pool connexions Manuel Intégré Intégré
Scalabilité Limitée (threads) Bonne Excellente
Cas d'usage Applications legacy Applications standard Applications réactives
Spring Boot Compatible Par défaut WebFlux

Recommandation :

  • JDBC simple : Prototypes, scripts, apprentissage
  • JDBC + HikariCP : Applications standard Spring Boot, APIs REST
  • R2DBC : Applications réactives haute scalabilité (Spring WebFlux, Reactor)

Partie 1 : JDBC (Java Database Connectivity)

Introduction à JDBC

JDBC est l'API standard Java pour interagir avec les bases de données relationnelles. Elle définit des interfaces que chaque SGBD implémente via un driver.

Architecture JDBC :

Application Java
      ↓
   JDBC API (java.sql.*)
      ↓
PostgreSQL JDBC Driver (org.postgresql)
      ↓
PostgreSQL Database

Installation et Dépendances

Maven (pom.xml)

<dependencies>
    <!-- PostgreSQL JDBC Driver -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.1</version>
    </dependency>
</dependencies>

Gradle (build.gradle)

dependencies {
    implementation 'org.postgresql:postgresql:42.7.1'
}

Connexion Basique à PostgreSQL

Connexion simple

import java.sql.Connection;  
import java.sql.DriverManager;  
import java.sql.SQLException;  

public class PostgreSQLConnection {
    public static void main(String[] args) {
        // URL de connexion JDBC
        String url = "jdbc:postgresql://localhost:5432/ma_database";
        String user = "mon_utilisateur";
        String password = "mon_password";

        Connection conn = null;

        try {
            // Établir la connexion
            conn = DriverManager.getConnection(url, user, password);
            System.out.println("✅ Connexion établie avec succès");

            // Votre code ici

        } catch (SQLException e) {
            System.err.println("❌ Erreur de connexion : " + e.getMessage());
            e.printStackTrace();
        } finally {
            // Toujours fermer la connexion
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Format de l'URL JDBC PostgreSQL :

jdbc:postgresql://[host]:[port]/[database]?[paramètres]

Exemples d'URLs :

// Local avec port par défaut
"jdbc:postgresql://localhost/mydb"

// Avec port spécifique
"jdbc:postgresql://localhost:5432/mydb"

// Avec SSL
"jdbc:postgresql://localhost/mydb?ssl=true&sslmode=require"

// Avec schéma spécifique
"jdbc:postgresql://localhost/mydb?currentSchema=public"

// Avec timeout
"jdbc:postgresql://localhost/mydb?loginTimeout=10&connectTimeout=10"

Try-with-resources (Java 7+, recommandé)

Le try-with-resources ferme automatiquement les ressources (Connection, Statement, ResultSet) :

public class JDBCExample {
    private static final String URL = "jdbc:postgresql://localhost:5432/mydb";
    private static final String USER = "user";
    private static final String PASSWORD = "password";

    public static void main(String[] args) {
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
            System.out.println("Connexion établie");
            // La connexion sera fermée automatiquement
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Avantage : Plus concis et plus sûr (pas d'oubli de fermeture).

Exécution de Requêtes

SELECT : Récupérer des données

import java.sql.*;

public class SelectExample {
    public static void main(String[] args) {
        String url = "jdbc:postgresql://localhost:5432/mydb";
        String user = "user";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, user, password)) {
            // Créer un Statement
            Statement stmt = conn.createStatement();

            // Exécuter une requête SELECT
            String query = "SELECT id, nom, email, age FROM utilisateurs";
            ResultSet rs = stmt.executeQuery(query);

            // Parcourir les résultats
            while (rs.next()) {
                int id = rs.getInt("id");
                String nom = rs.getString("nom");
                String email = rs.getString("email");
                int age = rs.getInt("age");

                System.out.printf("ID: %d, Nom: %s, Email: %s, Age: %d%n",
                                  id, nom, email, age);
            }

            // Fermer les ressources
            rs.close();
            stmt.close();

        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Avec try-with-resources (recommandé) :

try (Connection conn = DriverManager.getConnection(url, user, password);
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT * FROM utilisateurs")) {

    while (rs.next()) {
        System.out.println("Nom: " + rs.getString("nom"));
    }

} catch (SQLException e) {
    e.printStackTrace();
}

Prepared Statements (Sécurité Critique)

⚠️ JAMAIS comme ceci (VULNÉRABLE) :

// ❌ DANGEREUX - Injection SQL possible !
String email = "alice@example.com' OR '1'='1";  
Statement stmt = conn.createStatement();  
String query = "SELECT * FROM users WHERE email = '" + email + "'";  
ResultSet rs = stmt.executeQuery(query);  

✅ TOUJOURS utiliser PreparedStatement :

// ✅ SÉCURISÉ - Paramètres échappés automatiquement
String email = "alice@example.com";

try (Connection conn = DriverManager.getConnection(url, user, password);
     PreparedStatement pstmt = conn.prepareStatement(
         "SELECT * FROM users WHERE email = ?")) {

    // Définir les paramètres (index commence à 1)
    pstmt.setString(1, email);

    ResultSet rs = pstmt.executeQuery();

    while (rs.next()) {
        System.out.println("User: " + rs.getString("nom"));
    }

} catch (SQLException e) {
    e.printStackTrace();
}

Paramètres multiples :

String query = "SELECT * FROM produits WHERE categorie = ? AND prix > ?";

try (PreparedStatement pstmt = conn.prepareStatement(query)) {
    pstmt.setString(1, "Électronique");
    pstmt.setDouble(2, 100.0);

    ResultSet rs = pstmt.executeQuery();

    while (rs.next()) {
        System.out.println("Produit: " + rs.getString("nom"));
    }
}

Opérations CRUD Complètes

CREATE : Insérer des données

public class InsertExample {
    public static void insertUser(Connection conn, String nom, String email, int age)
            throws SQLException {

        String sql = "INSERT INTO utilisateurs (nom, email, age) VALUES (?, ?, ?)";

        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, nom);
            pstmt.setString(2, email);
            pstmt.setInt(3, age);

            int rowsAffected = pstmt.executeUpdate();
            System.out.println(rowsAffected + " ligne(s) insérée(s)");
        }
    }
}

INSERT avec RETURNING (récupérer l'ID généré) :

public static int insertUserWithId(Connection conn, String nom, String email, int age)
        throws SQLException {

    String sql = "INSERT INTO utilisateurs (nom, email, age) VALUES (?, ?, ?) RETURNING id";

    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setString(1, nom);
        pstmt.setString(2, email);
        pstmt.setInt(3, age);

        ResultSet rs = pstmt.executeQuery();

        if (rs.next()) {
            int newId = rs.getInt("id");
            System.out.println("✅ Utilisateur créé avec ID : " + newId);
            return newId;
        }
    }

    return -1;
}

Méthode standard Java (getGeneratedKeys) :

String sql = "INSERT INTO utilisateurs (nom, email, age) VALUES (?, ?, ?)";

try (PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
    pstmt.setString(1, "Alice");
    pstmt.setString(2, "alice@example.com");
    pstmt.setInt(3, 30);

    pstmt.executeUpdate();

    // Récupérer les clés générées
    ResultSet rs = pstmt.getGeneratedKeys();
    if (rs.next()) {
        int id = rs.getInt(1);
        System.out.println("ID généré : " + id);
    }
}

Insertion multiple (batch) :

public static void insertMultipleUsers(Connection conn, List<User> users)
        throws SQLException {

    String sql = "INSERT INTO utilisateurs (nom, email, age) VALUES (?, ?, ?)";

    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        // Désactiver l'autocommit pour le batch
        conn.setAutoCommit(false);

        for (User user : users) {
            pstmt.setString(1, user.getNom());
            pstmt.setString(2, user.getEmail());
            pstmt.setInt(3, user.getAge());
            pstmt.addBatch(); // Ajouter au batch
        }

        // Exécuter toutes les insertions
        int[] results = pstmt.executeBatch();
        conn.commit(); // Valider la transaction

        System.out.println(results.length + " utilisateurs insérés");

        // Rétablir l'autocommit
        conn.setAutoCommit(true);
    } catch (SQLException e) {
        conn.rollback(); // Annuler en cas d'erreur
        throw e;
    }
}

READ : Lire des données

public class User {
    private int id;
    private String nom;
    private String email;
    private int age;

    // Constructeurs, getters, setters...
}

public class UserRepository {
    // Récupérer un utilisateur par ID
    public static User getUserById(Connection conn, int id) throws SQLException {
        String sql = "SELECT * FROM utilisateurs WHERE id = ?";

        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, id);

            ResultSet rs = pstmt.executeQuery();

            if (rs.next()) {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setNom(rs.getString("nom"));
                user.setEmail(rs.getString("email"));
                user.setAge(rs.getInt("age"));
                return user;
            }
        }

        return null; // Utilisateur non trouvé
    }

    // Récupérer tous les utilisateurs
    public static List<User> getAllUsers(Connection conn) throws SQLException {
        List<User> users = new ArrayList<>();
        String sql = "SELECT * FROM utilisateurs ORDER BY nom";

        try (Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setNom(rs.getString("nom"));
                user.setEmail(rs.getString("email"));
                user.setAge(rs.getInt("age"));
                users.add(user);
            }
        }

        return users;
    }

    // Recherche avec filtres
    public static List<User> searchUsers(Connection conn, String nomPattern,
                                         int ageMin, int ageMax) throws SQLException {
        List<User> users = new ArrayList<>();
        String sql = "SELECT * FROM utilisateurs WHERE nom LIKE ? AND age BETWEEN ? AND ?";

        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, "%" + nomPattern + "%");
            pstmt.setInt(2, ageMin);
            pstmt.setInt(3, ageMax);

            ResultSet rs = pstmt.executeQuery();

            while (rs.next()) {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setNom(rs.getString("nom"));
                user.setEmail(rs.getString("email"));
                user.setAge(rs.getInt("age"));
                users.add(user);
            }
        }

        return users;
    }
}

UPDATE : Modifier des données

public static int updateUser(Connection conn, int id, String nom,
                             String email, int age) throws SQLException {

    String sql = "UPDATE utilisateurs SET nom = ?, email = ?, age = ? WHERE id = ?";

    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setString(1, nom);
        pstmt.setString(2, email);
        pstmt.setInt(3, age);
        pstmt.setInt(4, id);

        int rowsAffected = pstmt.executeUpdate();

        if (rowsAffected == 0) {
            System.out.println("⚠️ Utilisateur non trouvé");
        } else {
            System.out.println("✅ Utilisateur mis à jour");
        }

        return rowsAffected;
    }
}

// UPDATE partiel (uniquement les champs non-null)
public static int partialUpdateUser(Connection conn, int id, User updates)
        throws SQLException {

    StringBuilder sql = new StringBuilder("UPDATE utilisateurs SET ");
    List<Object> params = new ArrayList<>();

    // Construire dynamiquement la requête
    if (updates.getNom() != null) {
        sql.append("nom = ?, ");
        params.add(updates.getNom());
    }
    if (updates.getEmail() != null) {
        sql.append("email = ?, ");
        params.add(updates.getEmail());
    }
    if (updates.getAge() != 0) {
        sql.append("age = ?, ");
        params.add(updates.getAge());
    }

    // Retirer la dernière virgule
    sql.setLength(sql.length() - 2);
    sql.append(" WHERE id = ?");
    params.add(id);

    try (PreparedStatement pstmt = conn.prepareStatement(sql.toString())) {
        for (int i = 0; i < params.size(); i++) {
            pstmt.setObject(i + 1, params.get(i));
        }

        return pstmt.executeUpdate();
    }
}

DELETE : Supprimer des données

public static int deleteUser(Connection conn, int id) throws SQLException {
    String sql = "DELETE FROM utilisateurs WHERE id = ?";

    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setInt(1, id);

        int rowsAffected = pstmt.executeUpdate();

        if (rowsAffected == 0) {
            System.out.println("⚠️ Utilisateur non trouvé");
        } else {
            System.out.println("✅ Utilisateur supprimé");
        }

        return rowsAffected;
    }
}

// Suppression multiple
public static int deleteUsersByAge(Connection conn, int maxAge) throws SQLException {
    String sql = "DELETE FROM utilisateurs WHERE age < ?";

    try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setInt(1, maxAge);
        int rowsAffected = pstmt.executeUpdate();
        System.out.println(rowsAffected + " utilisateurs supprimés");
        return rowsAffected;
    }
}

Gestion des Transactions

Transaction basique

public static void transferMoney(Connection conn, int fromAccountId,
                                 int toAccountId, double amount)
        throws SQLException {

    try {
        // Désactiver l'autocommit
        conn.setAutoCommit(false);

        // Opération 1 : Débiter le compte source
        String debitSql = "UPDATE comptes SET solde = solde - ? WHERE id = ?";
        try (PreparedStatement pstmt = conn.prepareStatement(debitSql)) {
            pstmt.setDouble(1, amount);
            pstmt.setInt(2, fromAccountId);
            pstmt.executeUpdate();
        }

        // Opération 2 : Créditer le compte destination
        String creditSql = "UPDATE comptes SET solde = solde + ? WHERE id = ?";
        try (PreparedStatement pstmt = conn.prepareStatement(creditSql)) {
            pstmt.setDouble(1, amount);
            pstmt.setInt(2, toAccountId);
            pstmt.executeUpdate();
        }

        // Valider la transaction
        conn.commit();
        System.out.println("✅ Transfert réussi");

    } catch (SQLException e) {
        // Annuler la transaction en cas d'erreur
        conn.rollback();
        System.err.println("❌ Transfert annulé : " + e.getMessage());
        throw e;
    } finally {
        // Rétablir l'autocommit
        conn.setAutoCommit(true);
    }
}

Transaction avec SAVEPOINT

public static void complexTransaction(Connection conn) throws SQLException {
    Savepoint savepoint1 = null;

    try {
        conn.setAutoCommit(false);

        // Opération 1
        String sql1 = "INSERT INTO logs (message) VALUES (?)";
        try (PreparedStatement pstmt = conn.prepareStatement(sql1)) {
            pstmt.setString(1, "Opération 1");
            pstmt.executeUpdate();
        }

        // Créer un savepoint
        savepoint1 = conn.setSavepoint("savepoint1");

        try {
            // Opération 2 (risquée)
            String sql2 = "INSERT INTO risky_table (data) VALUES (?)";
            try (PreparedStatement pstmt = conn.prepareStatement(sql2)) {
                pstmt.setString(1, "data");
                pstmt.executeUpdate();
            }
        } catch (SQLException e) {
            // Revenir au savepoint (annule seulement l'Op 2)
            if (savepoint1 != null) {
                conn.rollback(savepoint1);
                System.out.println("Savepoint restauré, Op 1 toujours valide");
            }
        }

        // Opération 3
        String sql3 = "INSERT INTO logs (message) VALUES (?)";
        try (PreparedStatement pstmt = conn.prepareStatement(sql3)) {
            pstmt.setString(1, "Opération 3");
            pstmt.executeUpdate();
        }

        conn.commit();
        System.out.println("✅ Transaction validée");

    } catch (SQLException e) {
        conn.rollback();
        throw e;
    } finally {
        conn.setAutoCommit(true);
    }
}

Types de Données PostgreSQL

Types numériques

// INTEGER, BIGINT
pstmt.setInt(1, 100);  
pstmt.setLong(1, 1000000000L);  

// NUMERIC/DECIMAL (utiliser BigDecimal pour la précision)
import java.math.BigDecimal;  
pstmt.setBigDecimal(1, new BigDecimal("19.99"));  

// FLOAT/DOUBLE PRECISION
pstmt.setFloat(1, 3.14f);  
pstmt.setDouble(1, 3.14159);  

// SERIAL (auto-increment) - géré automatiquement par PostgreSQL

Types texte

// VARCHAR, TEXT
pstmt.setString(1, "Mon texte");

// CHAR (taille fixe)
pstmt.setString(1, "75001");

Types temporels

import java.sql.Date;  
import java.sql.Timestamp;  
import java.time.LocalDate;  
import java.time.LocalDateTime;  

// DATE
pstmt.setDate(1, Date.valueOf("2025-11-23"));
// Ou avec java.time (Java 8+)
pstmt.setObject(1, LocalDate.of(2025, 11, 23));

// TIMESTAMP
pstmt.setTimestamp(1, Timestamp.valueOf("2025-11-23 10:30:00"));
// Ou avec java.time
pstmt.setObject(1, LocalDateTime.now());

// TIMESTAMPTZ (avec timezone)
import java.time.ZonedDateTime;  
pstmt.setObject(1, ZonedDateTime.now());  

// Lecture
ResultSet rs = pstmt.executeQuery();  
if (rs.next()) {  
    LocalDate date = rs.getObject("date_column", LocalDate.class);
    LocalDateTime timestamp = rs.getObject("timestamp_column", LocalDateTime.class);
}

JSON et JSONB

import org.postgresql.util.PGobject;

// Insertion JSON
PGobject jsonObject = new PGobject();  
jsonObject.setType("jsonb");  
jsonObject.setValue("{\"nom\": \"Alice\", \"age\": 30, \"tags\": [\"dev\", \"sql\"]}");  

pstmt.setObject(1, jsonObject);

// Lecture JSON
ResultSet rs = pstmt.executeQuery();  
if (rs.next()) {  
    String json = rs.getString("profile");
    // Parser avec Jackson, Gson, etc.
    ObjectMapper mapper = new ObjectMapper();
    JsonNode node = mapper.readTree(json);
    String nom = node.get("nom").asText();
}

Avec Jackson (bibliothèque JSON populaire) :

import com.fasterxml.jackson.databind.ObjectMapper;

// Sérialisation
ObjectMapper mapper = new ObjectMapper();  
User user = new User("Alice", 30);  
String json = mapper.writeValueAsString(user);  

PGobject jsonb = new PGobject();  
jsonb.setType("jsonb");  
jsonb.setValue(json);  
pstmt.setObject(1, jsonb);  

// Désérialisation
ResultSet rs = pstmt.executeQuery();  
if (rs.next()) {  
    String jsonData = rs.getString("profile");
    User user = mapper.readValue(jsonData, User.class);
}

Arrays (Tableaux PostgreSQL)

import java.sql.Array;

// Insertion d'array
String[] tags = {"java", "postgresql", "jdbc"};  
Array sqlArray = conn.createArrayOf("text", tags);  
pstmt.setArray(1, sqlArray);  

// Lecture d'array
ResultSet rs = pstmt.executeQuery();  
if (rs.next()) {  
    Array array = rs.getArray("tags");
    String[] tags = (String[]) array.getArray();
    for (String tag : tags) {
        System.out.println(tag);
    }
}

UUID

import java.util.UUID;

// Génération UUID v4 côté Java
UUID uuid = UUID.randomUUID();  
pstmt.setObject(1, uuid);  

// Génération UUID v7 côté PostgreSQL (PG 18)
pstmt.executeUpdate("INSERT INTO events (id, data) VALUES (gen_uuid_v7(), ?)");

// Lecture UUID
ResultSet rs = pstmt.executeQuery();  
if (rs.next()) {  
    UUID id = (UUID) rs.getObject("id");
    // Ou
    String uuidString = rs.getString("id");
    UUID id2 = UUID.fromString(uuidString);
}

Gestion des Erreurs

Codes d'erreur PostgreSQL

try {
    // Exécution de requête
    pstmt.executeUpdate();

} catch (SQLException e) {
    System.err.println("Code erreur : " + e.getErrorCode());
    System.err.println("SQLSTATE : " + e.getSQLState());
    System.err.println("Message : " + e.getMessage());

    // Codes SQLSTATE courants
    switch (e.getSQLState()) {
        case "23505": // Unique violation
            System.err.println("Cette valeur existe déjà");
            break;
        case "23503": // Foreign key violation
            System.err.println("Référence introuvable");
            break;
        case "23502": // NOT NULL violation
            System.err.println("Champ obligatoire manquant");
            break;
        case "42P01": // Undefined table
            System.err.println("Table inexistante");
            break;
        case "42703": // Undefined column
            System.err.println("Colonne inexistante");
            break;
        default:
            System.err.println("Erreur PostgreSQL : " + e.getMessage());
    }
}

Gestion avancée avec exceptions personnalisées

public class DatabaseException extends Exception {
    private final String sqlState;

    public DatabaseException(String message, String sqlState) {
        super(message);
        this.sqlState = sqlState;
    }

    public String getSqlState() {
        return sqlState;
    }
}

public class UserRepository {
    public void createUser(Connection conn, User user) throws DatabaseException {
        try {
            String sql = "INSERT INTO utilisateurs (nom, email) VALUES (?, ?)";
            try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
                pstmt.setString(1, user.getNom());
                pstmt.setString(2, user.getEmail());
                pstmt.executeUpdate();
            }
        } catch (SQLException e) {
            if ("23505".equals(e.getSQLState())) {
                throw new DatabaseException("Email déjà utilisé", e.getSQLState());
            }
            throw new DatabaseException("Erreur lors de la création", e.getSQLState());
        }
    }
}

Partie 2 : HikariCP (Connection Pool)

Pourquoi un Pool de Connexions ?

Créer une nouvelle connexion PostgreSQL est coûteux :

  • Établissement de connexion TCP/IP
  • Authentification SSL
  • Allocation de ressources serveur
  • Temps : ~50-200ms par connexion

Un pool de connexions maintient un ensemble de connexions réutilisables, réduisant drastiquement ce coût.

Bénéfices :

  • Performance : Réduction de 80-95% du temps de connexion
  • 🎯 Contrôle : Limite le nombre de connexions actives
  • 📊 Monitoring : Métriques et statistiques intégrées
  • 🔒 Sécurité : Gestion automatique des connexions zombies

Installation HikariCP

Maven

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.1.0</version>
</dependency>

<!-- PostgreSQL Driver -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.1</version>
</dependency>

Gradle

implementation 'com.zaxxer:HikariCP:5.1.0'  
implementation 'org.postgresql:postgresql:42.7.1'  

Configuration Basique

import com.zaxxer.hikari.HikariConfig;  
import com.zaxxer.hikari.HikariDataSource;  

public class DatabaseConfig {
    private static HikariDataSource dataSource;

    public static HikariDataSource getDataSource() {
        if (dataSource == null) {
            HikariConfig config = new HikariConfig();

            // Configuration de connexion
            config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
            config.setUsername("user");
            config.setPassword("password");

            // Configuration du pool
            config.setMaximumPoolSize(10);       // Maximum 10 connexions
            config.setMinimumIdle(2);            // Minimum 2 connexions actives
            config.setConnectionTimeout(30000);   // Timeout 30s
            config.setIdleTimeout(600000);        // Fermer après 10min d'inactivité
            config.setMaxLifetime(1800000);       // Durée de vie max 30min

            // Optimisations
            config.setPoolName("PostgreSQLPool");
            config.addDataSourceProperty("cachePrepStmts", "true");
            config.addDataSourceProperty("prepStmtCacheSize", "250");
            config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

            dataSource = new HikariDataSource(config);
        }
        return dataSource;
    }

    public static void close() {
        if (dataSource != null && !dataSource.isClosed()) {
            dataSource.close();
        }
    }
}

Configuration Avancée

HikariConfig config = new HikariConfig();

// Connexion PostgreSQL
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");  
config.setUsername("user");  
config.setPassword("password");  
config.setDriverClassName("org.postgresql.Driver");  

// Taille du pool
config.setMaximumPoolSize(20);              // Max connexions (défaut: 10)  
config.setMinimumIdle(5);                   // Min connexions idle (défaut: maxPoolSize)  

// Timeouts
config.setConnectionTimeout(30000);         // 30s pour obtenir une connexion  
config.setIdleTimeout(600000);              // 10min avant fermeture connexion idle  
config.setMaxLifetime(1800000);             // 30min durée de vie max d'une connexion  
config.setKeepaliveTime(300000);            // 5min keepalive  

// Validation
config.setConnectionTestQuery("SELECT 1");  // Query de validation (optionnel)  
config.setValidationTimeout(5000);          // 5s pour valider une connexion  

// Monitoring et logs
config.setPoolName("PostgreSQL-Pool");  
config.setRegisterMbeans(true);             // JMX monitoring  
config.setLeakDetectionThreshold(60000);    // Détecter connexions non fermées (60s)  

// Propriétés PostgreSQL spécifiques
config.addDataSourceProperty("cachePrepStmts", "true");  
config.addDataSourceProperty("prepStmtCacheSize", "250");  
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");  
config.addDataSourceProperty("useServerPrepStmts", "true");  

HikariDataSource dataSource = new HikariDataSource(config);

Paramètres Recommandés par Cas d'Usage

Application Web Standard (OLTP)

config.setMaximumPoolSize(20);      // CPU cores * 2 + disk spindles  
config.setMinimumIdle(5);  
config.setConnectionTimeout(30000);  
config.setIdleTimeout(600000);  
config.setMaxLifetime(1800000);  

Microservice Léger

config.setMaximumPoolSize(5);  
config.setMinimumIdle(2);  
config.setConnectionTimeout(10000);  
config.setIdleTimeout(300000);  
config.setMaxLifetime(900000);  

Application Haute Charge

config.setMaximumPoolSize(50);  
config.setMinimumIdle(10);  
config.setConnectionTimeout(5000);  
config.setIdleTimeout(300000);  
config.setMaxLifetime(1200000);  

Utilisation avec JDBC

import java.sql.Connection;  
import java.sql.PreparedStatement;  
import java.sql.ResultSet;  

public class UserService {
    private final HikariDataSource dataSource;

    public UserService() {
        this.dataSource = DatabaseConfig.getDataSource();
    }

    public User findUserById(int id) throws SQLException {
        // Obtenir une connexion du pool
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(
                 "SELECT * FROM utilisateurs WHERE id = ?")) {

            pstmt.setInt(1, id);
            ResultSet rs = pstmt.executeQuery();

            if (rs.next()) {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setNom(rs.getString("nom"));
                user.setEmail(rs.getString("email"));
                return user;
            }

            return null;

        } // La connexion retourne automatiquement au pool
    }

    public void createUser(User user) throws SQLException {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(
                 "INSERT INTO utilisateurs (nom, email, age) VALUES (?, ?, ?)",
                 Statement.RETURN_GENERATED_KEYS)) {

            pstmt.setString(1, user.getNom());
            pstmt.setString(2, user.getEmail());
            pstmt.setInt(3, user.getAge());

            pstmt.executeUpdate();

            ResultSet rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                user.setId(rs.getInt(1));
            }
        }
    }
}

Monitoring et Métriques

import com.zaxxer.hikari.HikariPoolMXBean;

public class PoolMonitor {
    public static void printPoolStats(HikariDataSource dataSource) {
        HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();

        System.out.println("=== Pool Statistics ===");
        System.out.println("Active Connections: " + poolBean.getActiveConnections());
        System.out.println("Idle Connections: " + poolBean.getIdleConnections());
        System.out.println("Total Connections: " + poolBean.getTotalConnections());
        System.out.println("Threads Awaiting: " + poolBean.getThreadsAwaitingConnection());
    }

    // Monitoring périodique
    public static void startMonitoring(HikariDataSource dataSource) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        scheduler.scheduleAtFixedRate(() -> {
            printPoolStats(dataSource);
        }, 0, 30, TimeUnit.SECONDS);
    }
}

Exemple Complet avec HikariCP

import com.zaxxer.hikari.HikariConfig;  
import com.zaxxer.hikari.HikariDataSource;  
import java.sql.*;  
import java.util.ArrayList;  
import java.util.List;  

public class UserRepository {
    private final HikariDataSource dataSource;

    public UserRepository(String jdbcUrl, String username, String password) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(jdbcUrl);
        config.setUsername(username);
        config.setPassword(password);
        config.setMaximumPoolSize(10);
        config.setMinimumIdle(2);

        this.dataSource = new HikariDataSource(config);
    }

    public User create(User user) throws SQLException {
        String sql = "INSERT INTO utilisateurs (nom, email, age) VALUES (?, ?, ?) RETURNING *";

        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, user.getNom());
            pstmt.setString(2, user.getEmail());
            pstmt.setInt(3, user.getAge());

            ResultSet rs = pstmt.executeQuery();

            if (rs.next()) {
                return mapResultSetToUser(rs);
            }

            throw new SQLException("Échec de la création");
        }
    }

    public User findById(int id) throws SQLException {
        String sql = "SELECT * FROM utilisateurs WHERE id = ?";

        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setInt(1, id);
            ResultSet rs = pstmt.executeQuery();

            if (rs.next()) {
                return mapResultSetToUser(rs);
            }

            return null;
        }
    }

    public List<User> findAll() throws SQLException {
        List<User> users = new ArrayList<>();
        String sql = "SELECT * FROM utilisateurs ORDER BY nom";

        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                users.add(mapResultSetToUser(rs));
            }
        }

        return users;
    }

    public void update(int id, User user) throws SQLException {
        String sql = "UPDATE utilisateurs SET nom = ?, email = ?, age = ? WHERE id = ?";

        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setString(1, user.getNom());
            pstmt.setString(2, user.getEmail());
            pstmt.setInt(3, user.getAge());
            pstmt.setInt(4, id);

            int affected = pstmt.executeUpdate();

            if (affected == 0) {
                throw new SQLException("Utilisateur non trouvé");
            }
        }
    }

    public void delete(int id) throws SQLException {
        String sql = "DELETE FROM utilisateurs WHERE id = ?";

        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setInt(1, id);

            int affected = pstmt.executeUpdate();

            if (affected == 0) {
                throw new SQLException("Utilisateur non trouvé");
            }
        }
    }

    private User mapResultSetToUser(ResultSet rs) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setNom(rs.getString("nom"));
        user.setEmail(rs.getString("email"));
        user.setAge(rs.getInt("age"));
        return user;
    }

    public void close() {
        if (dataSource != null && !dataSource.isClosed()) {
            dataSource.close();
        }
    }
}

// Utilisation
public class Main {
    public static void main(String[] args) {
        UserRepository repo = new UserRepository(
            "jdbc:postgresql://localhost:5432/mydb",
            "user",
            "password"
        );

        try {
            // Créer
            User newUser = new User("Alice", "alice@example.com", 30);
            User created = repo.create(newUser);
            System.out.println("Créé : " + created.getId());

            // Lire
            User found = repo.findById(created.getId());
            System.out.println("Trouvé : " + found.getNom());

            // Modifier
            found.setAge(31);
            repo.update(found.getId(), found);

            // Lister
            List<User> all = repo.findAll();
            System.out.println("Total : " + all.size());

            // Supprimer
            repo.delete(created.getId());

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            repo.close();
        }
    }
}

Partie 3 : R2DBC (Reactive Relational Database Connectivity)

Introduction à R2DBC

R2DBC est une API réactive pour les bases de données relationnelles, basée sur le standard Reactive Streams. Contrairement à JDBC (bloquant), R2DBC est :

  • Non-bloquant : Les threads ne sont pas bloqués en attendant les réponses
  • Réactif : Basé sur les paradigmes Publisher/Subscriber (Reactor, RxJava)
  • Scalable : Meilleure utilisation des ressources pour les charges élevées

Quand utiliser R2DBC :

  • Applications Spring WebFlux (réactives)
  • Microservices haute scalabilité
  • Systèmes nécessitant des milliers de connexions concurrentes
  • Architecture event-driven

Quand NE PAS utiliser R2DBC :

  • Applications Spring MVC classiques
  • Équipes sans expérience de la programmation réactive
  • Applications CRUD simples (overhead inutile)

Installation R2DBC

Maven

<dependencies>
    <!-- R2DBC PostgreSQL Driver -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>r2dbc-postgresql</artifactId>
        <version>1.0.4.RELEASE</version>
    </dependency>

    <!-- R2DBC Pool -->
    <dependency>
        <groupId>io.r2dbc</groupId>
        <artifactId>r2dbc-pool</artifactId>
        <version>1.0.1.RELEASE</version>
    </dependency>

    <!-- Reactor Core (si pas déjà inclus) -->
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-core</artifactId>
        <version>3.6.0</version>
    </dependency>
</dependencies>

Concepts Réactifs Essentiels

Avant d'utiliser R2DBC, il faut comprendre les concepts de base de la programmation réactive :

Mono : 0 ou 1 élément

// Mono = Publisher qui émet 0 ou 1 élément
Mono<User> userMono = userRepository.findById(1);

// Souscription (c'est là que l'opération s'exécute)
userMono.subscribe(
    user -> System.out.println("Trouvé : " + user.getNom()),  // onNext
    error -> System.err.println("Erreur : " + error),          // onError
    () -> System.out.println("Terminé")                        // onComplete
);

Flux : 0 à N éléments

// Flux = Publisher qui émet 0 à N éléments
Flux<User> usersFlux = userRepository.findAll();

// Souscription
usersFlux.subscribe(
    user -> System.out.println("User : " + user.getNom()),
    error -> System.err.println("Erreur : " + error),
    () -> System.out.println("Tous les utilisateurs reçus")
);

Analogie :

  • Mono = Promesse JavaScript (résultat unique futur)
  • Flux = Observable RxJS (stream de résultats)

Configuration de Base

import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;  
import io.r2dbc.postgresql.PostgresqlConnectionFactory;  
import io.r2dbc.spi.ConnectionFactory;  

public class R2DBCConfig {
    public static ConnectionFactory createConnectionFactory() {
        PostgresqlConnectionConfiguration config = PostgresqlConnectionConfiguration.builder()
            .host("localhost")
            .port(5432)
            .database("mydb")
            .username("user")
            .password("password")
            .build();

        return new PostgresqlConnectionFactory(config);
    }
}

Configuration avec Pool

import io.r2dbc.pool.ConnectionPool;  
import io.r2dbc.pool.ConnectionPoolConfiguration;  
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;  
import io.r2dbc.postgresql.PostgresqlConnectionFactory;  

public class R2DBCPoolConfig {
    public static ConnectionPool createConnectionPool() {
        // Configuration PostgreSQL
        PostgresqlConnectionConfiguration pgConfig = PostgresqlConnectionConfiguration.builder()
            .host("localhost")
            .port(5432)
            .database("mydb")
            .username("user")
            .password("password")
            .build();

        ConnectionFactory connectionFactory = new PostgresqlConnectionFactory(pgConfig);

        // Configuration du pool
        ConnectionPoolConfiguration poolConfig = ConnectionPoolConfiguration.builder(connectionFactory)
            .maxSize(20)                        // Max 20 connexions
            .initialSize(5)                     // 5 connexions au démarrage
            .maxIdleTime(Duration.ofMinutes(30))
            .maxAcquireTime(Duration.ofSeconds(30))
            .maxCreateConnectionTime(Duration.ofSeconds(30))
            .build();

        return new ConnectionPool(poolConfig);
    }
}

CRUD avec R2DBC

CREATE : Insérer des données

import io.r2dbc.spi.Connection;  
import io.r2dbc.spi.ConnectionFactory;  
import reactor.core.publisher.Mono;  

public class UserRepository {
    private final ConnectionFactory connectionFactory;

    public UserRepository(ConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
    }

    public Mono<User> create(User user) {
        String sql = "INSERT INTO utilisateurs (nom, email, age) VALUES ($1, $2, $3) RETURNING *";

        return Mono.from(connectionFactory.create())
            .flatMap(connection ->
                Mono.from(connection.createStatement(sql)
                    .bind("$1", user.getNom())
                    .bind("$2", user.getEmail())
                    .bind("$3", user.getAge())
                    .execute())
                .flatMap(result -> Mono.from(result.map((row, metadata) -> {
                    User newUser = new User();
                    newUser.setId(row.get("id", Integer.class));
                    newUser.setNom(row.get("nom", String.class));
                    newUser.setEmail(row.get("email", String.class));
                    newUser.setAge(row.get("age", Integer.class));
                    return newUser;
                })))
                .doFinally(signalType -> connection.close())
            );
    }
}

// Utilisation
Mono<User> userMono = userRepository.create(new User("Alice", "alice@example.com", 30));

userMono.subscribe(
    user -> System.out.println("Créé : " + user.getId()),
    error -> System.err.println("Erreur : " + error)
);

READ : Lire des données

public Mono<User> findById(int id) {
    String sql = "SELECT * FROM utilisateurs WHERE id = $1";

    return Mono.from(connectionFactory.create())
        .flatMap(connection ->
            Mono.from(connection.createStatement(sql)
                .bind("$1", id)
                .execute())
            .flatMap(result -> Mono.from(result.map((row, metadata) -> {
                User user = new User();
                user.setId(row.get("id", Integer.class));
                user.setNom(row.get("nom", String.class));
                user.setEmail(row.get("email", String.class));
                user.setAge(row.get("age", Integer.class));
                return user;
            })))
            .doFinally(signalType -> connection.close())
        );
}

public Flux<User> findAll() {
    String sql = "SELECT * FROM utilisateurs ORDER BY nom";

    return Mono.from(connectionFactory.create())
        .flatMapMany(connection ->
            Flux.from(connection.createStatement(sql).execute())
                .flatMap(result -> result.map((row, metadata) -> {
                    User user = new User();
                    user.setId(row.get("id", Integer.class));
                    user.setNom(row.get("nom", String.class));
                    user.setEmail(row.get("email", String.class));
                    user.setAge(row.get("age", Integer.class));
                    return user;
                }))
                .doFinally(signalType -> connection.close())
        );
}

UPDATE : Modifier des données

public Mono<Integer> update(int id, User user) {
    String sql = "UPDATE utilisateurs SET nom = $1, email = $2, age = $3 WHERE id = $4";

    return Mono.from(connectionFactory.create())
        .flatMap(connection ->
            Mono.from(connection.createStatement(sql)
                .bind("$1", user.getNom())
                .bind("$2", user.getEmail())
                .bind("$3", user.getAge())
                .bind("$4", id)
                .execute())
            .flatMap(result -> Mono.from(result.getRowsUpdated()))
            .doFinally(signalType -> connection.close())
        );
}

DELETE : Supprimer des données

public Mono<Integer> delete(int id) {
    String sql = "DELETE FROM utilisateurs WHERE id = $1";

    return Mono.from(connectionFactory.create())
        .flatMap(connection ->
            Mono.from(connection.createStatement(sql)
                .bind("$1", id)
                .execute())
            .flatMap(result -> Mono.from(result.getRowsUpdated()))
            .doFinally(signalType -> connection.close())
        );
}

Transactions Réactives

public Mono<Void> transferMoney(int fromAccountId, int toAccountId, double amount) {
    return Mono.from(connectionFactory.create())
        .flatMap(connection ->
            // Démarrer la transaction
            Mono.from(connection.beginTransaction())
                .then(
                    // Opération 1 : Débiter
                    Mono.from(connection.createStatement(
                        "UPDATE comptes SET solde = solde - $1 WHERE id = $2")
                        .bind("$1", amount)
                        .bind("$2", fromAccountId)
                        .execute())
                    .flatMap(result -> Mono.from(result.getRowsUpdated()))
                )
                .then(
                    // Opération 2 : Créditer
                    Mono.from(connection.createStatement(
                        "UPDATE comptes SET solde = solde + $1 WHERE id = $2")
                        .bind("$1", amount)
                        .bind("$2", toAccountId)
                        .execute())
                    .flatMap(result -> Mono.from(result.getRowsUpdated()))
                )
                .then(Mono.from(connection.commitTransaction()))  // Commit
                .onErrorResume(error ->
                    Mono.from(connection.rollbackTransaction())   // Rollback
                        .then(Mono.error(error))
                )
                .doFinally(signalType -> connection.close())
        );
}

Exemple Complet avec R2DBC

import io.r2dbc.pool.ConnectionPool;  
import io.r2dbc.spi.Connection;  
import reactor.core.publisher.Flux;  
import reactor.core.publisher.Mono;  

public class ReactiveUserRepository {
    private final ConnectionPool connectionPool;

    public ReactiveUserRepository(ConnectionPool connectionPool) {
        this.connectionPool = connectionPool;
    }

    public Mono<User> create(User user) {
        String sql = "INSERT INTO utilisateurs (nom, email, age) VALUES ($1, $2, $3) RETURNING *";

        return Mono.from(connectionPool.create())
            .flatMap(connection ->
                executeAndMapSingle(connection, sql, user)
                    .doFinally(sig -> connection.close())
            );
    }

    public Mono<User> findById(int id) {
        String sql = "SELECT * FROM utilisateurs WHERE id = $1";

        return Mono.from(connectionPool.create())
            .flatMap(connection ->
                Mono.from(connection.createStatement(sql)
                    .bind("$1", id)
                    .execute())
                .flatMap(result -> Mono.from(result.map(this::mapRowToUser)))
                .doFinally(sig -> connection.close())
            );
    }

    public Flux<User> findAll() {
        String sql = "SELECT * FROM utilisateurs ORDER BY nom";

        return Mono.from(connectionPool.create())
            .flatMapMany(connection ->
                Flux.from(connection.createStatement(sql).execute())
                    .flatMap(result -> result.map(this::mapRowToUser))
                    .doFinally(sig -> connection.close())
            );
    }

    public Mono<Integer> update(int id, User user) {
        String sql = "UPDATE utilisateurs SET nom = $1, email = $2, age = $3 WHERE id = $4";

        return Mono.from(connectionPool.create())
            .flatMap(connection ->
                Mono.from(connection.createStatement(sql)
                    .bind("$1", user.getNom())
                    .bind("$2", user.getEmail())
                    .bind("$3", user.getAge())
                    .bind("$4", id)
                    .execute())
                .flatMap(result -> Mono.from(result.getRowsUpdated()))
                .doFinally(sig -> connection.close())
            );
    }

    public Mono<Integer> delete(int id) {
        String sql = "DELETE FROM utilisateurs WHERE id = $1";

        return Mono.from(connectionPool.create())
            .flatMap(connection ->
                Mono.from(connection.createStatement(sql)
                    .bind("$1", id)
                    .execute())
                .flatMap(result -> Mono.from(result.getRowsUpdated()))
                .doFinally(sig -> connection.close())
            );
    }

    private User mapRowToUser(Row row, RowMetadata metadata) {
        User user = new User();
        user.setId(row.get("id", Integer.class));
        user.setNom(row.get("nom", String.class));
        user.setEmail(row.get("email", String.class));
        user.setAge(row.get("age", Integer.class));
        return user;
    }

    private Mono<User> executeAndMapSingle(Connection connection, String sql, User user) {
        return Mono.from(connection.createStatement(sql)
            .bind("$1", user.getNom())
            .bind("$2", user.getEmail())
            .bind("$3", user.getAge())
            .execute())
        .flatMap(result -> Mono.from(result.map(this::mapRowToUser)));
    }
}

// Utilisation
public class Main {
    public static void main(String[] args) {
        ConnectionPool pool = R2DBCPoolConfig.createConnectionPool();
        ReactiveUserRepository repo = new ReactiveUserRepository(pool);

        // Créer un utilisateur
        User newUser = new User("Alice", "alice@example.com", 30);

        repo.create(newUser)
            .flatMap(created -> {
                System.out.println("Créé : " + created.getId());
                // Récupérer l'utilisateur
                return repo.findById(created.getId());
            })
            .flatMap(found -> {
                System.out.println("Trouvé : " + found.getNom());
                // Modifier
                found.setAge(31);
                return repo.update(found.getId(), found)
                    .thenReturn(found);
            })
            .flatMapMany(updated -> {
                // Lister tous
                return repo.findAll();
            })
            .doOnNext(user -> System.out.println("User : " + user.getNom()))
            .then()
            .doFinally(sig -> pool.dispose())
            .block(); // Bloquer pour l'exemple (éviter en prod)
    }
}

Comparaison : JDBC vs JDBC+HikariCP vs R2DBC

Performance

Benchmark indicatif (1000 requêtes SELECT) :

  • JDBC simple : ~2000ms
  • JDBC + HikariCP : ~300ms (6-7× plus rapide)
  • R2DBC : ~250ms (8× plus rapide)

Note : Les gains R2DBC sont surtout visibles sous forte charge concurrente.

Scalabilité

Approche Connexions concurrentes Modèle
JDBC simple Limité (1 thread = 1 connexion) Bloquant
JDBC + HikariCP Bon (pool optimisé) Bloquant
R2DBC Excellent (milliers) Non-bloquant

Complexité du Code

JDBC + HikariCP :

// Code impératif simple
try (Connection conn = pool.getConnection()) {
    PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
    pstmt.setInt(1, 1);
    ResultSet rs = pstmt.executeQuery();
    if (rs.next()) {
        return new User(rs.getInt("id"), rs.getString("nom"));
    }
}

R2DBC :

// Code réactif (plus verbeux)
return Mono.from(pool.create())
    .flatMap(connection ->
        Mono.from(connection.createStatement("SELECT * FROM users WHERE id = $1")
            .bind("$1", 1)
            .execute())
        .flatMap(result -> Mono.from(result.map((row, meta) ->
            new User(row.get("id", Integer.class), row.get("nom", String.class))
        )))
        .doFinally(sig -> connection.close())
    );

Quand Utiliser Chaque Approche

JDBC + HikariCP ✅

Choisir si :

  • Application Spring Boot MVC classique
  • Équipe expérimentée en Java mais pas en réactif
  • Application CRUD standard
  • Migration depuis JDBC existant
  • Besoin de stabilité et maturité

Exemples :

  • API REST Spring Boot
  • Application web e-commerce
  • Système de gestion interne

R2DBC ✅

Choisir si :

  • Application Spring WebFlux (réactive)
  • Besoin de haute scalabilité (milliers de connexions)
  • Architecture event-driven
  • Microservices haute performance
  • Équipe à l'aise avec la programmation réactive

Exemples :

  • API Gateway réactif
  • Système de streaming de données
  • Chat en temps réel
  • IoT avec millions de devices

Bonnes Pratiques

1. Toujours Utiliser un Pool de Connexions

Éviter :

// Créer une nouvelle connexion à chaque requête
Connection conn = DriverManager.getConnection(url, user, pass);

Bon :

// Utiliser HikariCP
HikariDataSource dataSource = new HikariDataSource(config);  
Connection conn = dataSource.getConnection();  

2. Toujours Utiliser PreparedStatement

Dangereux :

Statement stmt = conn.createStatement();  
stmt.executeQuery("SELECT * FROM users WHERE email = '" + email + "'");  

Sécurisé :

PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE email = ?");  
pstmt.setString(1, email);  

3. Fermer les Ressources (try-with-resources)

Éviter :

Connection conn = dataSource.getConnection();  
PreparedStatement pstmt = conn.prepareStatement(sql);  
// Oubli de fermeture = fuite de ressources

Bon :

try (Connection conn = dataSource.getConnection();
     PreparedStatement pstmt = conn.prepareStatement(sql)) {
    // Ressources fermées automatiquement
}

4. Configurer Correctement le Pool

// Configuration adaptée à votre charge
config.setMaximumPoolSize(20);              // Pas trop haut (surcharge DB)  
config.setMinimumIdle(5);                   // Connexions toujours prêtes  
config.setConnectionTimeout(30000);         // Timeout raisonnable  
config.setLeakDetectionThreshold(60000);    // Détecter les fuites  

5. Gérer les Erreurs Proprement

try {
    // Opérations DB
} catch (SQLException e) {
    // Logger l'erreur
    logger.error("Erreur DB: {}", e.getMessage(), e);

    // Traiter selon le code d'erreur
    if ("23505".equals(e.getSQLState())) {
        throw new DuplicateKeyException("Valeur déjà existante");
    }

    // Rethrow ou wrapper
    throw new DatabaseException("Erreur lors de l'opération", e);
}

6. Utiliser des Transactions pour les Opérations Multiples

try (Connection conn = dataSource.getConnection()) {
    conn.setAutoCommit(false);

    try {
        // Opération 1
        // Opération 2
        // Opération 3

        conn.commit();
    } catch (Exception e) {
        conn.rollback();
        throw e;
    }
}

7. Monitoring et Logging

// HikariCP : activer JMX monitoring
config.setRegisterMbeans(true);

// Log des requêtes lentes
config.setLeakDetectionThreshold(60000);

// Log périodique des stats
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);  
scheduler.scheduleAtFixedRate(() -> {  
    HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();
    logger.info("Pool stats: Active={}, Idle={}, Total={}",
        poolBean.getActiveConnections(),
        poolBean.getIdleConnections(),
        poolBean.getTotalConnections());
}, 0, 60, TimeUnit.SECONDS);

Ressources et Documentation

JDBC

HikariCP

R2DBC

Spring Boot


Résumé des Points Clés

JDBC

  • ✅ API standard Java pour bases de données
  • ✅ Utilisez PreparedStatement pour éviter les injections SQL
  • Try-with-resources pour fermer automatiquement les ressources
  • ✅ Gestion des transactions avec setAutoCommit(false), commit(), rollback()
  • ✅ Support complet de tous les types PostgreSQL

HikariCP

  • Pool de connexions le plus performant en Java
  • ✅ Configuration simple et intuitive
  • Obligatoire pour les applications en production
  • ✅ Monitoring intégré (JMX)
  • ✅ Utilisé par défaut dans Spring Boot

R2DBC

  • ✅ API réactive et non-bloquante
  • ✅ Basé sur Reactor (Mono/Flux)
  • ✅ Idéal pour Spring WebFlux et architectures réactives
  • Scalabilité exceptionnelle (milliers de connexions)
  • ✅ Plus complexe que JDBC (courbe d'apprentissage)

Choix Final

Pour 90% des projets : JDBC + HikariCP (stable, mature, performant)
Pour applications réactives : R2DBC (scalabilité maximale)
Pour prototypes/scripts : JDBC simple (sans pool)


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

⏭️ Go : pgx, GORM