🔝 Retour au Sommaire
Java dispose de plusieurs approches pour interagir avec PostgreSQL, chacune adaptée à différents besoins et architectures :
- JDBC : API standard Java pour l'accès aux bases de données (synchrone/bloquant)
- HikariCP : Pool de connexions haute performance pour JDBC
- 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.
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 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 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.
| 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)
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
<dependencies>
<!-- PostgreSQL JDBC Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.1</version>
</dependency>
</dependencies>dependencies {
implementation 'org.postgresql:postgresql:42.7.1'
}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"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).
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();
}// ❌ 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"));
}
}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;
}
}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;
}
}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();
}
}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;
}
}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);
}
}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);
}
}// 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// VARCHAR, TEXT
pstmt.setString(1, "Mon texte");
// CHAR (taille fixe)
pstmt.setString(1, "75001");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);
}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);
}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);
}
}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);
}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());
}
}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());
}
}
}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
<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>implementation 'com.zaxxer:HikariCP:5.1.0'
implementation 'org.postgresql:postgresql:42.7.1' 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();
}
}
}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);config.setMaximumPoolSize(20); // CPU cores * 2 + disk spindles
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000); config.setMaximumPoolSize(5);
config.setMinimumIdle(2);
config.setConnectionTimeout(10000);
config.setIdleTimeout(300000);
config.setMaxLifetime(900000); config.setMaximumPoolSize(50);
config.setMinimumIdle(10);
config.setConnectionTimeout(5000);
config.setIdleTimeout(300000);
config.setMaxLifetime(1200000); 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));
}
}
}
}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);
}
}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();
}
}
}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)
<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>Avant d'utiliser R2DBC, il faut comprendre les concepts de base de la programmation réactive :
// 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 = 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)
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);
}
}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);
}
}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)
);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())
);
}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())
);
}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())
);
}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())
);
}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)
}
}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.
| Approche | Connexions concurrentes | Modèle |
|---|---|---|
| JDBC simple | Limité (1 thread = 1 connexion) | Bloquant |
| JDBC + HikariCP | Bon (pool optimisé) | Bloquant |
| R2DBC | Excellent (milliers) | Non-bloquant |
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())
);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
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
❌ É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(); ❌ 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); ❌ É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
}// 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 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);
}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;
}
}// 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);- Documentation Oracle JDBC : https://docs.oracle.com/javase/tutorial/jdbc/
- PostgreSQL JDBC Driver : https://jdbc.postgresql.org/
- Documentation officielle : https://github.com/brettwooldridge/HikariCP
- Configuration Guide : https://github.com/brettwooldridge/HikariCP/wiki/Configuration
- Documentation officielle : https://r2dbc.io/
- R2DBC PostgreSQL : https://github.com/pgjdbc/r2dbc-postgresql
- Project Reactor : https://projectreactor.io/
- Spring Data JDBC : https://spring.io/projects/spring-data-jdbc
- Spring Data R2DBC : https://spring.io/projects/spring-data-r2dbc
- ✅ 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
- ✅ 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
- ✅ 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)
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.