Introduction à JDBC : Fondamentaux de la Connectivité Java-Base de Données
JDBC (Java Database Connectivity) est une API standard Java qui permet aux applications Java d'interagir avec différentes bases de données. Il s'agit d'un ensemble d'interfaces définies dans le paquetage java.sql et javax.sql, dont les implémentations sont fournies par les fournisseurs de bases de données sous forme de pilotes (drivers). Cette approche basée sur des interfaces illustre parfaitement les avantages de la programmation orientée interface, offrant une abstraction complète de la base de données sous-jacente.
Configuration Initiale et Première Connexion
Avant d'exécuter des opérations sur une base de données, quelques étapes préliminaires sont nécessaires :
- Préparation de la base de données : Assurez-vous d'avoir une instance de base de données (par exemple, MySQL, PostgreSQL) fonctionnelle avec les tables et les données nécessaires.
- Téléchargement du pilote JDBC : Procurez-vous le fichier JAR du pilote JDBC spécifique à votre base de données auprès du site web du fournisseur (par exemple, MySQL Connector/J).
- Intégration au projet Java : Ajoutez le fichier JAR du pilote au chemin de classe (classpath) de votre projet Java. Dans un environnement Maven/Gradle, cela se fait via une dépendance.
Le processus de connexion à une base de données via JDBC implique typiquement six étapes fondamentales : l'enregistrement du pilote, l'établissement de la connexion, la création d'une instruction, l'exécution de la requête SQL, le traitement des résultats et la libération des ressources.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class BasicJdbcExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/ma_base_de_donnees";
private static final String DB_USER = "utilisateur";
private static final String DB_PASSWORD = "motdepasse";
public static void main(String[] args) {
// Étape 1: Le pilote se charge automatiquement avec les versions modernes de JDBC
// Class.forName("com.mysql.cj.jdbc.Driver"); // N'est plus nécessaire explicitement pour les versions récentes
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, nom, prix FROM produits")) { // Exemple de requête
// Étape 5: Traitement du jeu de résultats
System.out.println("Liste des produits:");
while (rs.next()) {
int idProduit = rs.getInt("id");
String nomProduit = rs.getString("nom");
double prixProduit = rs.getDouble("prix");
System.out.println("ID: " + idProduit + ", Nom: " + nomProduit + ", Prix: " + prixProduit);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
L'exemple ci-dessus utilise un Statement, qui est simple pour des requêtes statiques. Cependant, il est fortement recommandé d'utiliser PreparedStatement pour toutes les requêtes, notamment celles avec des paramètres variables, afin de prévenir les injections SQL et d'améliorer les performances grâce à la pré-compilation des requêtes.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;
public class PreparedJdbcExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/ma_base_de_donnees";
private static final String DB_USER = "utilisateur";
private static final String DB_PASSWORD = "motdepasse";
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
Scanner scanner = new Scanner(System.in)) {
System.out.print("Entrez l'ID du produit à rechercher : ");
int idRecherche = scanner.nextInt();
String sqlQuery = "SELECT id, nom, prix FROM produits WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sqlQuery)) {
pstmt.setInt(1, idRecherche); // Définition du paramètre pour le placeholder '?'
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
int idProduit = rs.getInt("id");
String nomProduit = rs.getString("nom");
double prixProduit = rs.getDouble("prix");
System.out.println("Produit trouvé: ID=" + idProduit + ", Nom=" + nomProduit + ", Prix=" + prixProduit);
} else {
System.out.println("Aucun produit trouvé avec l'ID " + idRecherche);
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Interfaces JDBC Essentielles
Connection: Représente une session unique avec une base de données. Elle est responsable de la gestion des transactions (commit(),rollback()) et de la création des objetsStatementetPreparedStatement. L'URL de connexion, le nom d'utilisateur et le mot de passe sont des paramètres clés lors de son établissement.Statement: Utilisé pour exécuter des requêtes SQL statiques sans paramètres. Il est déconseillé pour les requêtes dynamiques en raison du risque d'injection SQL.PreparedStatement: Une sous-interface deStatement, optimisée pour l'exécution répétée de requêtes SQL pré-compilées. Elle accepte des paramètres via des placeholders (?), prévenant ainsi les injections SQL et améliorant les performances.ResultSet: Contient le jeu de résultats d'une requête SELECT. Il permet de parcourir les lignes de données (avecnext()) et d'extraire les valeurs des colonnes par leur nom ou leur index (getInt(),getString(), etc.).
Opérations CRUD avec PreparedStatement
L'utilisation de PreparedStatement pour les opérations CRUD (Create, Read, Update, Delete) est une pratique standard.
Lecture de Données (SELECT)
Pour récupérer plusieurs enregistrements basés sur un critère :
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class CrudOperations {
private static final String DB_URL = "jdbc:mysql://localhost:3306/ma_base_de_donnees";
private static final String DB_USER = "utilisateur";
private static final String DB_PASSWORD = "motdepasse";
public static void findProductsUnderPrice(double maxPrice) {
String sql = "SELECT id, nom, prix, stock FROM produits WHERE prix < ?";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setDouble(1, maxPrice);
try (ResultSet rs = pstmt.executeQuery()) {
System.out.println("Produits dont le prix est inférieur à " + maxPrice + " :");
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("nom");
double price = rs.getDouble("prix");
int stock = rs.getInt("stock");
System.out.println("ID: " + id + ", Nom: " + name + ", Prix: " + price + ", Stock: " + stock);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
findProductsUnderPrice(200.00);
}
}
Insertion, Mise à Jour et Suppression de Données (INSERT, UPDATE, DELETE)
Ces opérations utilisent la méthode executeUpdate() de PreparedStatement, qui retourne le nombre de lignes affectées.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class CrudOperations {
private static final String DB_URL = "jdbc:mysql://localhost:3306/ma_base_de_donnees";
private static final String DB_USER = "utilisateur";
private static final String DB_PASSWORD = "motdepasse";
// ... (méthode findProductsUnderPrice) ...
public static void addProduct(String name, double price, int stock) {
String sql = "INSERT INTO produits (nom, prix, stock) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, name);
pstmt.setDouble(2, price);
pstmt.setInt(3, stock);
int affectedRows = pstmt.executeUpdate();
if (affectedRows > 0) {
System.out.println("Produit '" + name + "' ajouté avec succès.");
} else {
System.out.println("Échec de l'ajout du produit.");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void updateProductPrice(int productId, double newPrice) {
String sql = "UPDATE produits SET prix = ? WHERE id = ?";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setDouble(1, newPrice);
pstmt.setInt(2, productId);
int affectedRows = pstmt.executeUpdate();
if (affectedRows > 0) {
System.out.println("Prix du produit ID " + productId + " mis à jour à " + newPrice + ".");
} else {
System.out.println("Aucun produit trouvé avec l'ID " + productId + " pour la mise à jour.");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void removeProduct(int productId) {
String sql = "DELETE FROM produits WHERE id = ?";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, productId);
int affectedRows = pstmt.executeUpdate();
if (affectedRows > 0) {
System.out.println("Produit ID " + productId + " supprimé avec succès.");
} else {
System.out.println("Aucun produit trouvé avec l'ID " + productId + " pour la suppression.");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// Exemples d'utilisation
addProduct("Clavier mécanique", 120.50, 50);
updateProductPrice(1, 130.00); // Supposons que l'ID 1 existe
removeProduct(2); // Supposons que l'ID 2 existe
findProductsUnderPrice(200.00);
}
}
Concepts Avancés en JDBC
Mapping Objet-Relationnel (ORM) Manuel
En Java, les données sont généralement manipulées sous forme d'objets. L'approche JDBC brute renvoie des données tbaulaires, ce qui nécessite un travail manuel pour les mapper aux objets Java correspondants. C'est le principe du Mapping Objet-Relationnel (ORM) : une table de base de données est mappée à une classe Java, une ligne de la table à une instance de cette classe, et une colonne à une propriété de l'objet.
Considérons une classe Produit :
public class Produit {
private int id;
private String nom;
private double prix;
private int stock;
// Constructeurs
public Produit() {}
public Produit(int id, String nom, double prix, int stock) {
this.id = id;
this.nom = nom;
this.prix = prix;
this.stock = stock;
}
// Getters et Setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; }
public double getPrix() { return prix; }
public void setPrix(double prix) { this.prix = prix; }
public int getStock() { return stock; }
public void setStock(int stock) { this.stock = stock; }
@Override
public String toString() {
return "Produit{" +
"id=" + id +
", nom='" + nom + '\'' +
", prix=" + prix +
", stock=" + stock +
'}';
}
}
Pour récupérer une liste d'objets Produit :
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class OrmExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/ma_base_de_donnees";
private static final String DB_USER = "utilisateur";
private static final String DB_PASSWORD = "motdepasse";
public static List<produit> getAllProducts() {
List<produit> produits = new ArrayList<>();
String sql = "SELECT id, nom, prix, stock FROM produits";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
Produit p = new Produit();
p.setId(rs.getInt("id"));
p.setNom(rs.getString("nom"));
p.setPrix(rs.getDouble("prix"));
p.setStock(rs.getInt("stock"));
produits.add(p);
}
} catch (SQLException e) {
e.printStackTrace();
}
return produits;
}
public static void main(String[] args) {
List<produit> allProducts = getAllProducts();
System.out.println("Tous les produits : " + allProducts);
}
}
</produit></produit></produit>
Récupération de Clés Générées Automatiquement
Lors de l'insertion d'un nouvel enregistrement avec une colonne clé primaire auto-incrémentée, il est souvent nécessaire de récupérer cette clé générée par la base de données. JDBC permet de le faire en spécifiant l'option Statement.RETURN_GENERATED_KEYS lors de la création du PreparedStatement.
import java.sql.*;
public class KeyGenerationExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/ma_base_de_donnees";
private static final String DB_USER = "utilisateur";
private static final String DB_PASSWORD = "motdepasse";
public static Produit addNewProductAndGetId(String name, double price, int stock) {
Produit newProduct = new Produit(0, name, price, stock); // ID sera défini par la DB
String sql = "INSERT INTO produits (nom, prix, stock) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(1, newProduct.getNom());
pstmt.setDouble(2, newProduct.getPrix());
pstmt.setInt(3, newProduct.getStock());
int affectedRows = pstmt.executeUpdate();
if (affectedRows > 0) {
try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
int generatedId = generatedKeys.getInt(1); // Récupérer la première clé générée
newProduct.setId(generatedId);
System.out.println("Produit ajouté avec l'ID généré : " + generatedId);
}
}
} else {
System.out.println("Échec de l'ajout du produit.");
}
} catch (SQLException e) {
e.printStackTrace();
}
return newProduct;
}
public static void main(String[] args) {
Produit p = addNewProductAndGetId("Souris gaming", 75.99, 100);
System.out.println("Nouveau produit : " + p);
}
}
Pool de Connexions : Optimisation des Performances
L'établissement et la fermeture répétée de connexions à une base de données sont des opérations coûteuses en ressources. Un pool de connexions (connection pool) pré-alloue un ensemble de connexions et les gère, les rendant disponibles à la demande et les recyclant une fois utilisées. Cela réduit considérablement le temps de latence et la charge sur le serveur de base de données.
L'interface javax.sql.DataSource est la norme Java pour les pools de connexions. Des implémentations populaires incluent HikariCP (souvent intégré à Spring Boot), Apache DBCP, C3P0 et Druid.
Exemple avec HikariCP (nécessite la dépendance com.zaxxer:HikariCP) :
Configuration "en dur" (Hardcoded)
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class HikariCpHardcoded {
public static void main(String[] args) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/ma_base_de_donnees");
config.setUsername("utilisateur");
config.setPassword("motdepasse");
config.setDriverClassName("com.mysql.cj.jdbc.Driver"); // Facultatif pour les versions récentes de JDBC
config.setMinimumIdle(5); // Nombre minimum de connexions inactives
config.setMaximumPoolSize(20); // Taille maximale du pool
try (HikariDataSource dataSource = new HikariDataSource(config);
Connection conn = dataSource.getConnection()) {
System.out.println("Connexion obtenue depuis le pool : " + conn);
// Effectuer des opérations DB...
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Configuration via fichier de propriétés (méthode recommandée)
Créez un fichier hikari.properties dans votre dossier de ressources (par exemple, src/main/resources) :
jdbcUrl=jdbc:mysql://localhost:3306/ma_base_de_donnees
username=utilisateur
password=motdepasse
driverClassName=com.mysql.cj.jdbc.Driver
minimumIdle=5
maximumPoolSize=20
Puis, chargez ce fichier dans votre code :
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class HikariCpProperties {
public static void main(String[] args) {
Properties props = new Properties();
try (InputStream input = HikariCpProperties.class.getClassLoader().getResourceAsStream("hikari.properties")) {
if (input == null) {
System.err.println("Le fichier hikari.properties est introuvable.");
return;
}
props.load(input);
} catch (IOException ex) {
ex.printStackTrace();
return;
}
HikariConfig config = new HikariConfig(props);
try (HikariDataSource dataSource = new HikariDataSource(config);
Connection conn = dataSource.getConnection()) {
System.out.println("Connexion obtenue depuis le pool (via propriétés) : " + conn);
// Effectuer des opérations DB...
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Utilisation de l'objet Properties
La classe java.util.Properties est un conteneur clé-valeur idéal pour charger des configurations à partir de fichiers .properties. Elle étend java.util.Hashtable<Object, Object> mais est spécifiquement conçue pour stocker des paires clé-valeur de type String.
getProperty(String key): Récupère la valeur associée à une clé.setProperty(String key, String value): Définit une paire clé-valeur.load(InputStream inStream): Charge les propriétés depuis un flux d'entrée.store(OutputStream out, String comments): Enregistre les propriétés dans un flux de sortie.stringPropertyNames(): Retourne un ensemble de toutes les clés de propriété sous forme de chaînes.
Conception Avancée : Utilitaires et Couches DAO
Encapsulation de la Logique JDBC : Classe Utilitaire de Connexion
Pour centraliser la gestion des connexions et éviter la répétition de code, il est courant de créer une classe utilitaire. Cette classe gérera l'initialisation du pool de connexions et fournira des méthodes statiques pour obtenir et libérer des connexoins.
DatabaseConnectionManager (Version simple)
Ce gestionnaire expose une méthode pour obtenir une connexion du pool et une autre pour la libérer (la retourner au pool).
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class DatabaseConnectionManager {
private static HikariDataSource dataSource;
static {
// Initialisation du pool de connexions au démarrage de l'application
Properties props = new Properties();
try (InputStream input = DatabaseConnectionManager.class.getClassLoader().getResourceAsStream("hikari.properties")) {
if (input == null) {
throw new IOException("Le fichier hikari.properties est introuvable.");
}
props.load(input);
} catch (IOException e) {
throw new RuntimeException("Erreur lors du chargement des propriétés du pool de connexions", e);
}
HikariConfig config = new HikariConfig(props);
dataSource = new HikariDataSource(config);
}
/**
* Obtient une connexion du pool.
* @return Une instance de Connection.
* @throws SQLException Si une erreur SQL survient.
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* Libère une connexion (la retourne au pool).
* @param conn La connexion à libérer.
*/
public static void releaseConnection(Connection conn) {
if (conn != null) {
try {
conn.close(); // La fermeture retourne la connexion au pool
} catch (SQLException e) {
System.err.println("Erreur lors de la libération de la connexion : " + e.getMessage());
}
}
}
}
DatabaseConnectionManager (Avec ThreadLocal pour les transactions)
Pour garantir qu'un même thread utilise la même connexion pour plusieurs opérations (nécessaire pour les transactions), on peut utiliser ThreadLocal.
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class DatabaseConnectionManagerWithThreadLocal {
private static HikariDataSource dataSource;
private static ThreadLocal<Connection> threadLocalConnection = new ThreadLocal<>();
static {
Properties props = new Properties();
try (InputStream input = DatabaseConnectionManagerWithThreadLocal.class.getClassLoader().getResourceAsStream("hikari.properties")) {
if (input == null) {
throw new IOException("Le fichier hikari.properties est introuvable.");
}
props.load(input);
} catch (IOException e) {
throw new RuntimeException("Erreur lors du chargement des propriétés du pool de connexions", e);
}
HikariConfig config = new HikariConfig(props);
dataSource = new HikariDataSource(config);
}
/**
* Obtient une connexion. Si une connexion est déjà associée au thread, elle est retournée.
* Sinon, une nouvelle connexion est prise du pool et associée au thread.
* @return Une instance de Connection.
* @throws SQLException Si une erreur SQL survient.
*/
public static Connection getConnection() throws SQLException {
Connection conn = threadLocalConnection.get();
if (conn == null) {
conn = dataSource.getConnection();
threadLocalConnection.set(conn);
}
return conn;
}
/**
* Libère la connexion associée au thread et la retourne au pool.
*/
public static void releaseConnection() {
Connection conn = threadLocalConnection.get();
if (conn != null) {
try {
conn.close(); // La fermeture retourne la connexion au pool
} catch (SQLException e) {
System.err.println("Erreur lors de la libération de la connexion : " + e.getMessage());
} finally {
threadLocalConnection.remove(); // Supprime la connexion du ThreadLocal
}
}
}
}
La Couche DAO et le AbstractDataAccessObject
Le pattern DAO (Data Access Object) sépare la logique d'accès aux données de la logique métier. Pour chaque entité métier (par exemple, Produit), un DAO est créé pour gérer ses opérations CRUD. Pour éviter la duplication de code entre les DAOs, un AbstractDataAccessObject générique peut être mis en place.
Ce AbstractDataAccessObject gérera la création de PreparedStatement, l'affectation des paramètres, l'exécution des requêtes et le mappage des résultats vers des objets Java, souvent à l'aide de la réflexion.
AbstractDataAccessObject (Base DAO)
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractDataAccessObject {
/**
* Exécute une requête non-SELECT (INSERT, UPDATE, DELETE).
* @param sql La requête SQL.
* @param params Les paramètres de la requête.
* @return Le nombre de lignes affectées.
* @throws SQLException Si une erreur SQL survient.
*/
protected int executeNonQuery(String sql, Object... params) throws SQLException {
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = DatabaseConnectionManagerWithThreadLocal.getConnection();
pstmt = conn.prepareStatement(sql);
setParameters(pstmt, params);
return pstmt.executeUpdate();
} finally {
if (pstmt != null) pstmt.close();
// Ne pas fermer la connexion ici si elle est gérée par ThreadLocal pour des transactions
// DatabaseConnectionManagerWithThreadLocal.releaseConnection(); // Appeler cela à la fin de la transaction
}
}
/**
* Exécute une requête SELECT et retourne une liste d'objets T.
* @param entityClass La classe de l'entité à mapper.
* @param sql La requête SQL.
* @param params Les paramètres de la requête.
* @param <T> Le type de l'entité.
* @return Une liste d'objets de type T.
* @throws SQLException Si une erreur SQL survient.
* @throws InstantiationException Si l'instanciation de T échoue.
* @throws IllegalAccessException Si l'accès aux champs de T échoue.
* @throws NoSuchFieldException Si un champ correspondant au nom de colonne n'est pas trouvé.
*/
protected <T> List<T> executeQueryList(Class<T> entityClass, String sql, Object... params)
throws SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
List<T> results = new ArrayList<>();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DatabaseConnectionManagerWithThreadLocal.getConnection();
pstmt = conn.prepareStatement(sql);
setParameters(pstmt, params);
rs = pstmt.executeQuery();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
while (rs.next()) {
T entity = entityClass.newInstance();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i); // Utilise le label de la colonne
Object value = rs.getObject(i);
try {
Field field = entityClass.getDeclaredField(columnName);
field.setAccessible(true);
field.set(entity, value);
} catch (NoSuchFieldException e) {
// Ignorer les colonnes qui n'ont pas de champ correspondant dans la classe
System.err.println("Le champ " + columnName + " n'existe pas dans la classe " + entityClass.getName());
}
}
results.add(entity);
}
} finally {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
// DatabaseConnectionManagerWithThreadLocal.releaseConnection(); // Appeler cela à la fin de la transaction
}
return results;
}
/**
* Exécute une requête SELECT et retourne un seul objet T.
* @param entityClass La classe de l'entité à mapper.
* @param sql La requête SQL.
* @param params Les paramètres de la requête.
* @param <T> Le type de l'entité.
* @return Un objet de type T, ou null si aucun résultat.
* @throws SQLException Si une erreur SQL survient.
* @throws InstantiationException Si l'instanciation de T échoue.
* @throws IllegalAccessException Si l'accès aux champs de T échoue.
* @throws NoSuchFieldException Si un champ correspondant au nom de colonne n'est pas trouvé.
*/
protected <T> T executeQuerySingle(Class<T> entityClass, String sql, Object... params)
throws SQLException, InstantiationException, IllegalAccessException, NoSuchFieldException {
List<T> results = executeQueryList(entityClass, sql, params);
return results.isEmpty() ? null : results.get(0);
}
// Méthode utilitaire pour définir les paramètres d'un PreparedStatement
private void setParameters(PreparedStatement pstmt, Object... params) throws SQLException {
if (params != null && params.length > 0) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
}
}
Implémentation DAO pour l'entité Produit
L'interface définit les opérations spécifiques à l'entité, et l'implémentation étend le AbstractDataAccessObject.
import java.util.List;
import java.sql.SQLException;
public interface ProductDAO {
List<Produit> findAll() throws SQLException;
Produit findById(int id) throws SQLException;
int insert(Produit produit) throws SQLException;
int update(Produit produit) throws SQLException;
int delete(int id) throws SQLException;
}
// Implémentation
import java.sql.SQLException;
import java.util.List;
public class ProductDAOImpl extends AbstractDataAccessObject implements ProductDAO {
@Override
public List<Produit> findAll() throws SQLException {
try {
return executeQueryList(Produit.class, "SELECT id, nom, prix, stock FROM produits");
} catch (InstantiationException | IllegalAccessException | NoSuchFieldException e) {
throw new SQLException("Erreur lors du mappage des produits", e);
}
}
@Override
public Produit findById(int id) throws SQLException {
try {
return executeQuerySingle(Produit.class, "SELECT id, nom, prix, stock FROM produits WHERE id = ?", id);
} catch (InstantiationException | IllegalAccessException | NoSuchFieldException e) {
throw new SQLException("Erreur lors du mappage du produit", e);
}
}
@Override
public int insert(Produit produit) throws SQLException {
return executeNonQuery("INSERT INTO produits (nom, prix, stock) VALUES (?, ?, ?)",
produit.getNom(), produit.getPrix(), produit.getStock());
}
@Override
public int update(Produit produit) throws SQLException {
return executeNonQuery("UPDATE produits SET nom = ?, prix = ?, stock = ? WHERE id = ?",
produit.getNom(), produit.getPrix(), produit.getStock(), produit.getId());
}
@Override
public int delete(int id) throws SQLException {
return executeNonQuery("DELETE FROM produits WHERE id = ?", id);
}
}
Gestion des Transactions en JDBC
Les transactions garantissent l'intégrité des données en regroupant plusieurs opérations de base de données en une seule unité atomique. Si une opération échoue, toutes les modifications effectuées dans la transaction sont annulées.
Par défaut, JDBC est en mode auto-commit (chaque instruction SQL est une transaction distincte). Pour gérer les transactions manuellement :
- Désactiver l'auto-commit :
connection.setAutoCommit(false); - Exécuter les opérations SQL.
- Si toutes les opérations réussissent, valider la transaction :
connection.commit(); - Si une erreur survient, annuler la transaction :
connection.rollback(); - Veiller à ce que la connexion soit toujours libérée dans un bloc
finally.
Exemple de transfert de stock entre deux produits :
import java.sql.Connection;
import java.sql.SQLException;
public class TransactionExample {
private static final ProductDAO productDAO = new ProductDAOImpl();
public static void transferStock(int fromProductId, int toProductId, int quantity) throws SQLException {
Connection conn = null;
try {
conn = DatabaseConnectionManagerWithThreadLocal.getConnection();
conn.setAutoCommit(false); // Désactiver l'auto-commit
// Diminuer le stock du produit source
Produit fromProduct = productDAO.findById(fromProductId);
if (fromProduct == null || fromProduct.getStock() < quantity) {
throw new SQLException("Stock insuffisant ou produit source introuvable.");
}
fromProduct.setStock(fromProduct.getStock() - quantity);
productDAO.update(fromProduct);
// Simuler une erreur
// if (true) throw new RuntimeException("Erreur simulée après première mise à jour.");
// Augmenter le stock du produit destinataire
Produit toProduct = productDAO.findById(toProductId);
if (toProduct == null) {
throw new SQLException("Produit destinataire introuvable.");
}
toProduct.setStock(toProduct.getStock() + quantity);
productDAO.update(toProduct);
conn.commit(); // Valider la transaction si tout réussit
System.out.println("Transfert de stock effectué avec succès.");
} catch (SQLException | RuntimeException e) {
if (conn != null) {
conn.rollback(); // Annuler la transaction en cas d'erreur
System.err.println("Transaction annulée en raison d'une erreur : " + e.getMessage());
}
throw new SQLException("Échec du transfert de stock.", e);
} finally {
if (conn != null) {
conn.setAutoCommit(true); // Rétablir l'auto-commit
}
DatabaseConnectionManagerWithThreadLocal.releaseConnection(); // Libérer la connexion du thread
}
}
public static void main(String[] args) {
// Préparer des produits pour le test (si la base est vide)
ProductDAO setupDao = new ProductDAOImpl();
try {
setupDao.insert(new Produit(0, "Article A", 10.0, 100));
setupDao.insert(new Produit(0, "Article B", 20.0, 50));
} catch (SQLException e) {
System.err.println("Impossible d'initialiser les produits : " + e.getMessage());
} finally {
DatabaseConnectionManagerWithThreadLocal.releaseConnection();
}
try {
// IDs des produits à transférer, supposons 1 et 2 existent
transferStock(1, 2, 10);
} catch (SQLException e) {
System.err.println("Erreur principale : " + e.getMessage());
}
// Vérifier les stocks après l'opération
try {
System.out.println("Produit 1 après transfert: " + productDAO.findById(1));
System.out.println("Produit 2 après transfert: " + productDAO.findById(2));
} catch (SQLException e) {
e.printStackTrace();
} finally {
DatabaseConnectionManagerWithThreadLocal.releaseConnection();
}
}
}