Accès avancé aux bases de données relationnelles en Java (JDBC)

Université de Toulon

LIS UMR CNRS 7020

2024-12-13

Optimisation des Connexions à la Base de Données

Pour optimiser les performances et l’utilisation des ressources, réutilisez les connexions à la base de données. Deux approches courantes :

  1. DataSource
    • Utilisez une implémentation de DataSource pour gérer les connexions centralisées.
  2. Pooling de Connexions
    • Utilisez des bibliothèques de pooling de connexions comme Apache DBCP et C3P0 pour réduire les coûts d’ouverture/fermeture des connexions.
    • Réduction de la latence, meilleure gestion des ressources.

L’utilisation est simple on définit une classe qui paramètre une datasource qui gère un pool de connexion et fournit des connections :

%maven org.apache.commons:commons-dbcp2:2.13.0
import org.apache.commons.dbcp2.BasicDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class DBCPDataSource {

    // Création d'un pool de connexions avec BasicDataSource
    private static BasicDataSource ds = new BasicDataSource();

    static {
        // Configuration des paramètres de connexion
        ds.setUrl("jdbc:postgresql://docker/gtfs"); // URL de la base de données
        ds.setUsername(System.getenv("DB_USERNAME")); // Nom d'utilisateur (récupéré depuis les variables d'environnement)
        ds.setPassword(System.getenv("DB_PASSWORD")); // Mot de passe (récupéré depuis les variables d'environnement)

        // Configuration du pool de connexions
        ds.setMinIdle(5); // Nombre minimal de connexions inactives dans le pool
        ds.setMaxIdle(10); // Nombre maximal de connexions inactives dans le pool
        ds.setMaxOpenPreparedStatements(100); // Nombre maximal de requêtes préparées ouvertes simultanément
    }

    // Constructeur privé pour empêcher l'instanciation directe de la classe
    private DBCPDataSource() {
    }

    // Méthode statique pour obtenir une connexion à partir du pool
    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
}

Il suffit ensuite de demander et de rendre une connexion (le close ne ferme pas forcément la connexion).

// Liste des identifiants d'arrêts à rechercher
List<String> stopIds = Arrays.asList("TOCMAS", "TOCMAN");

// Démonstration de l'utilisation du pool de connexions :
// - Chaque itération demande/rend une connexion au pool
// - Le pool évite la création/destruction coûteuse de connexions
// - Les connexions sont réutilisées en interne par le pool
for (String stopId : stopIds) {
    // Utilisation de try-with-resources pour garantir la fermeture des ressources
    try (
        // Obtention d'une connexion depuis le pool (rapide car connexion déjà établie)
        Connection connection = DBCPDataSource.getConnection();
        // Création d'un PreparedStatement (optimisation et sécurité) :
        // - Protection contre les injections SQL
        // - Mise en cache du plan d'exécution par la BD
        // - Réutilisation possible de la requête compilée
        PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM stops WHERE stop_id = ?")
    ) {
        // Paramétrage sécurisé de la requête
        pstmt.setString(1, stopId);  // Les indices commencent à 1

        // Exécution de la requête et récupération des résultats
        try (ResultSet rs = pstmt.executeQuery()) {
            System.out.printf("%nRésultats pour stop_id = %s:%n", stopId);
            if (rs.next()) {
                // Affichage formaté des résultats
                System.out.println("%s (%s)".formatted(
                        rs.getString("stop_id"),
                        rs.getString("stop_name")));
            } else {
                System.out.printf("Aucun arrêt trouvé avec l'ID %s%n", stopId);
            }
        }
    } catch (SQLException e) {
        // Gestion des erreurs avec information contextuelle
        System.err.printf("Erreur lors du traitement de l'ID %s: %s%n", 
            stopId, e.getMessage());
    }
    // À la fin du bloc try, les ressources sont automatiquement fermées :
    // - Le ResultSet est fermé
    // - Le PreparedStatement est fermé
    // - La connexion est rendue au pool (mais pas fermée physiquement)
}

Résultats pour stop_id = TOCMAS:
TOCMAS (Champ de Mars)

Résultats pour stop_id = TOCMAN:
TOCMAN (Champ de Mars)

La couche DAO (Data Access Object) en Java JDBC

  • Objectif de la couche DAO (Data Access Object)
    • Abstraire l’accès aux données pour isoler la logique métier de la persistance.
    • Centraliser les opérations spécifiques liées à la persistance des données.
  • Associations entre entités et classes DAO
    • Chaque entité (par exemple, une table dans la base de données) peut être associée à une classe DAO.
    • Évite la répétition de code pour les opérations CRUD (Create, Read, Update, Delete).
  • Implémentation de base pour une DAO
    1. Interface générique :
      • Définir une interface avec les méthodes CRUD (create, read, update, delete).
      • Exemple : DAO<T>.
    2. Classe générique :
      • Implémenter cette interface dans une classe par entité (par exemple, DogDAO implements DAO<Dog>).
      • Utiliser une classe asbtraite pour factoriser le code commun.
      • Utiliser des requêtes SQL paramétrées pour les opérations.
      • Gérer les exceptions spécifiques (transformer les exceptions SQL en exceptions applicatives).

Interface générique DAO

public interface GenericDAO<T, ID> {
    void create(T entity) throws SQLException;
    T read(ID id) throws SQLException;
    void update(T entity) throws SQLException;
    void delete(ID id) throws SQLException;
    List<T> findAll() throws SQLException;
}

Implantation concrète DAO

// Record User pour représenter l'entité utilisateur
public record User(Integer id, String name, String email) {}

// Implémentation concrète de UserDAO
public class UserDAO implements GenericDAO<User, Integer> {
    private Connection connection;

    public UserDAO(Connection connection) {
        this.connection = connection;
    }

    @Override
    public void create(User user) throws SQLException {
        String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
        try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
            pstmt.setString(1, user.name());
            pstmt.setString(2, user.email());
            pstmt.executeUpdate();
        }
    }

    @Override
    public User read(Integer id) throws SQLException {
        String sql = "SELECT * FROM users WHERE id = ?";
        try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
            pstmt.setInt(1, id);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return new User(rs.getInt("id"), rs.getString("name"), rs.getString("email"));
                }
            }
        }
        return null;
    }

    @Override
    public void update(User user) throws SQLException {
        String sql = "UPDATE users SET name = ?, email = ? WHERE id = ?";
        try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
            pstmt.setString(1, user.name());
            pstmt.setString(2, user.email());
            pstmt.setInt(3, user.id());
            pstmt.executeUpdate();
        }
    }

    @Override
    public void delete(Integer id) throws SQLException {
        String sql = "DELETE FROM users WHERE id = ?";
        try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
            pstmt.setInt(1, id);
            pstmt.executeUpdate();
        }
    }

    @Override
    public List<User> findAll() throws SQLException {
        List<User> users = new ArrayList<>();
        String sql = "SELECT * FROM users";
        try (Statement stmt = connection.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            while (rs.next()) {
                users.add(new User(rs.getInt("id"), rs.getString("name"), rs.getString("email")));
            }
        }
        return users;
    }
}

Création d’une base de données simple

%maven com.h2database:h2:2.3.232
%maven com.github.javafaker:javafaker:1.0.2
import com.github.javafaker.Faker;
private static final String DB_URL = "jdbc:h2:mem:testdb";
private static final String DB_USERNAME = "sa";
private static final String DB_PASSWORD = "";

Class.forName("org.h2.Driver");

private static void createTables(Connection connection) throws SQLException {
    String createUsersTableSql = "CREATE TABLE users (" +
            "id INT AUTO_INCREMENT PRIMARY KEY," +
            "name VARCHAR(255) NOT NULL," +
            "email VARCHAR(255) NOT NULL)";
    try (Statement statement = connection.createStatement()) {
        statement.execute(createUsersTableSql);
    }
}

private static void populateTables(Connection connection) throws SQLException {
    Faker faker = new Faker();
    String insertUsersSql = "INSERT INTO users (name, email) VALUES (?, ?)";

    try (PreparedStatement pstmt = connection.prepareStatement(insertUsersSql)) {
        IntStream.range(0, 10).forEach(i -> {
            try {
                pstmt.setString(1, faker.name().fullName());
                pstmt.setString(2, faker.internet().emailAddress());
                pstmt.executeUpdate();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

Utilisation de la DAO

try (Connection connection = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)) {
    createTables(connection);
    populateTables(connection);
    System.out.println("Database setup completed successfully.");

    // Utilisation de UserDAO pour effectuer des opérations CRUD
    UserDAO userDAO = new UserDAO(connection);

    // Création d'utilisateurs
    userDAO.create(new User(null, "Alice", "alice@example.com"));
    userDAO.create(new User(null, "Bob", "bob@example.com"));

    // Lecture d'un utilisateur
    User user = userDAO.read(1);
    System.out.println("Read user: " + user);

    // Mise à jour d'un utilisateur
    userDAO.update(new User(1, "Alice Updated", "alice.updated@example.com"));

    // Suppression d'un utilisateur
    userDAO.delete(2);

    // Lecture de tous les utilisateurs
    List<User> users = userDAO.findAll();
    users.forEach(System.out::println);

} catch (SQLException e) {
    e.printStackTrace();
}
Database setup completed successfully.
Read user: User[id=1, name=Angelica Ryan Sr., email=virginia.stokes@hotmail.com]
User[id=1, name=Alice Updated, email=alice.updated@example.com]
User[id=3, name=Miss Domonique Windler, email=serita.hudson@hotmail.com]
User[id=4, name=Nella Morissette, email=danny.koch@gmail.com]
User[id=5, name=Connie Gutmann I, email=evan.mills@yahoo.com]
User[id=6, name=Antione McCullough, email=larae.connelly@gmail.com]
User[id=7, name=Fermin Schmeler Jr., email=georgette.vonrueden@hotmail.com]
User[id=8, name=Daine Kovacek, email=rebeca.weber@hotmail.com]
User[id=9, name=Ivey Johnson Jr., email=dewayne.marquardt@hotmail.com]
User[id=10, name=Minh Zboncak, email=luanna.rodriguez@yahoo.com]
User[id=11, name=Alice, email=alice@example.com]
User[id=12, name=Bob, email=bob@example.com]

Metadata

  • JDBC permet de récupérer des informations sur le type de données que l’on vient de récupérer par un SELECT (interface ResultSetMetaData),
  • mais aussi sur la base de données elle-même (interface DatabaseMetaData)
  • Les données que l’on peut récupérer avec DatabaseMetaData dépendent du SGBD avec lequel on travaille

Exemple de récupération des métadonnées de la base de données

try (Connection connection = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)) {

    // Récupération des métadonnées de la base de données
    DatabaseMetaData metadata = connection.getMetaData();

    // Affichage des informations sur le système de gestion de base de données
    System.out.printf("DBMS: %s %d.%d%n",
            metadata.getDatabaseProductName(),
            metadata.getDatabaseMajorVersion(),
            metadata.getDatabaseMinorVersion());

    // Affichage de la base de données et du schéma actuels
    System.out.printf("Current Database: %s Schema: %s%n",
            connection.getCatalog(), connection.getSchema());

    // Récupération de la liste des tables pour vérifier l'existence de "SIMPLEJDBC_PERSON"
    ResultSet tables = metadata.getTables(connection.getCatalog(),
            connection.getSchema(),
            "SIMPLEJDBC_PERSON", null);

    if (tables.next()) {
        // Si la table existe déjà, affichage d'un message
        System.out.println("Table " + tables.getString("TABLE_NAME") + " already exists.");
    } else {
        // Si la table n'existe pas, création de la table "SIMPLEJDBC_PERSON"
        String createTableSql = "CREATE TABLE \"SIMPLEJDBC_PERSON\"(" +
                "id INT PRIMARY KEY NOT NULL, " +
                "firstname VARCHAR(100))";
        try (Statement statement = connection.createStatement()) {
            statement.execute(createTableSql);
            System.out.println("Table SIMPLEJDBC_PERSON created.");
        }
    }

} catch (SQLException e) {
    // Gestion des exceptions SQL
    e.printStackTrace();
}
DBMS: PostgreSQL 17.2
Current Database: gtfs Schema: public
Table SIMPLEJDBC_PERSON already exists.

Exemple de récupération des métadonnées du ResultSet

// Connexion à la base de données et récupération des métadonnées du ResultSet
try (Connection connection = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
     ResultSet rs = connection.createStatement().executeQuery("SELECT * FROM stops")) {

    // Récupération de l'objet ResultSetMetaData pour obtenir des informations sur les colonnes
    ResultSetMetaData rsmd = rs.getMetaData();
    int nbColonnes = rsmd.getColumnCount();

    // Parcours des colonnes et affichage des informations
    for (int i = 1; i <= nbColonnes; i++) {
        String typeColonne = rsmd.getColumnTypeName(i); // Type de la colonne
        String nomColonne = rsmd.getColumnName(i);      // Nom de la colonne
        System.out.printf("Colonne %d: nom = %s, type = %s%n", i, nomColonne, typeColonne);
    }
} catch (SQLException e) {
    // Gestion des exceptions SQL
    e.printStackTrace();
}
Colonne 1: nom = stop_id, type = text
Colonne 2: nom = stop_code, type = text
Colonne 3: nom = stop_name, type = text
Colonne 4: nom = stop_desc, type = text
Colonne 5: nom = stop_loc, type = geography
Colonne 6: nom = zone_id, type = text
Colonne 7: nom = stop_url, type = text
Colonne 8: nom = location_type, type = location_type_val
Colonne 9: nom = parent_station, type = text
Colonne 10: nom = stop_timezone, type = text
Colonne 11: nom = wheelchair_boarding, type = wheelchair_boarding_val
Colonne 12: nom = level_id, type = text
Colonne 13: nom = platform_code, type = text

Exemple de récupération des métadonnées des colonnes d’une table

import java.sql.*;
import java.util.stream.IntStream;

// Connexion à la base de données et récupération des métadonnées du ResultSet
try (Connection connection = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
     ResultSet rs = connection.createStatement().executeQuery("SELECT * FROM stops LIMIT 5")) {

    // Récupération de l'objet ResultSetMetaData pour obtenir des informations sur les colonnes
    ResultSetMetaData rsmd = rs.getMetaData();
    int nbColonnes = rsmd.getColumnCount();

    // Affichage des noms de colonnes
    IntStream.range(1, nbColonnes + 1)
             .mapToObj(i -> {
                 try {
                     return rsmd.getColumnName(i)+" "+rsmd.getColumnTypeName(i);
                 } catch (SQLException e) {
                     throw new RuntimeException(e);
                 }
             })
             .forEach(nomColonne -> System.out.printf("%-20s", nomColonne));
    System.out.println();

    // Affichage des données des lignes
    while (rs.next()) {
        IntStream.range(1, nbColonnes + 1)
                 .mapToObj(i -> {
                     try {
                         return rs.getString(i);
                     } catch (SQLException e) {
                         throw new RuntimeException(e);
                     }
                 })
                 .forEach(valeur -> System.out.printf("%-20s", valeur));
        System.out.println();
    }

} catch (SQLException e) {
    // Gestion des exceptions SQL
    e.printStackTrace();
}
stop_id text        stop_code text      stop_name text      stop_desc text      stop_loc geography  zone_id text        stop_url text       location_type location_type_valparent_station text stop_timezone text  wheelchair_boarding wheelchair_boarding_vallevel_id text       platform_code text  
BNBACE              null                Bassin Castigneau   null                0101000020E61000001D739EB12FA91740959C137B688F4540null                null                stop                null                null                accessible          null                null                
BNBACO              null                Bassin Castigneau   null                0101000020E6100000B14E95EF19A91740A6D3BA0D6A8F4540null                null                stop                null                null                accessible          null                null                
BNBVIE              null                Base de Vie Ouest   null                0101000020E610000093C83EC8B2A0174083328D26178F4540null                null                stop                null                null                accessible          null                null                
BNBVIO              null                Base de Vie Ouest   null                0101000020E610000060CC96AC8AA017401E6D1CB1168F4540null                null                stop                null                null                accessible          null                null                
BNCASN              null                Porte Castigneau    null                0101000020E610000050A8A78FC0AF1740A13193A817904540null                null                stop                null                null                accessible          null                null                

ResultSet modifiables

Il est aussi possible de paramétrer un ResultSet pour qu’il soit modifiable pendant la consultation et/ou que l’on puisse revenir en arrière.

try (Connection connection = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD);
     ResultSet rs = connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)
         .executeQuery("SELECT * FROM agency")) {

    // Affichage des données initiales
    System.out.println("Données initiales:");
    while (rs.next()) {
        System.out.printf("ID: %s, Nom: %s%n", rs.getString("agency_id"), rs.getString("agency_name"));
    }

    // Mise à jour d'un enregistrement
    rs.beforeFirst(); // Revenir au début du ResultSet
    while (rs.next()) {
        if (rs.getString("agency_name").equalsIgnoreCase("Réseau Mistral")) {
            rs.updateString("agency_name", "Agence Université Updated");
            rs.updateRow();
        }
    }

    // Ajout d'un nouvel enregistrement
    rs.moveToInsertRow();
    rs.updateString("agency_id", "9999");
    rs.updateString("agency_name", "New Agency");
    rs.updateString("agency_url", "http://newagency.com");
    rs.updateString("agency_timezone", "Europe/Paris");
    rs.insertRow();

    // Suppression d'un enregistrement
    rs.beforeFirst(); // Revenir au début du ResultSet
    while (rs.next()) {
        if (rs.getString("agency_name").equalsIgnoreCase("Neverland")) {
            rs.deleteRow();
        }
    }

    // Affichage des données mises à jour
    rs.beforeFirst(); // Revenir au début du ResultSet
    System.out.println("Données mises à jour:");
    while (rs.next()) {
        System.out.printf("ID: %s, Nom: %s%n", rs.getString("agency_id"), rs.getString("agency_name"));
    }

} catch (SQLException e) {
    // Gestion des exceptions SQL
    e.printStackTrace();
}
Données initiales:
ID: 3, Nom: Neverland
ID: 9999, Nom: New Agency
ID: 1, Nom: Agence Université Updated
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "agency_pkey"
  Detail: Key (agency_id)=(9999) already exists.
    at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2725)
    at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2412)
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:371)
    at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:502)
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:419)
    at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:194)
    at org.postgresql.jdbc.PgPreparedStatement.executeUpdate(PgPreparedStatement.java:155)
    at org.postgresql.jdbc.PgResultSet.insertRow(PgResultSet.java:1199)
    at REPL.$JShell$168.do_it$($JShell$168.java:78)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at io.github.spencerpark.ijava.execution.IJavaExecutionControl.lambda$execute$1(IJavaExecutionControl.java:95)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    at java.base/java.lang.Thread.run(Thread.java:1583)

Batch Update

Principe et avantages - Optimisation des performances pour les opérations multiples - Réduction des échanges avec la base de données - Amélioration significative des temps de traitement

Points de vigilance - Gestion explicite des transactions requise (auto-commit désactivé) - Risque de saturation mémoire avec de trop grands lots - Nécessité de prévoir un mécanisme de rollback

Bonnes pratiques - Utiliser des PreparedStatement pour les requêtes - Définir une taille de lot adaptée (100-1000 éléments) - Exécuter périodiquement pour les grands volumes de données - Implémenter une gestion d’erreurs robuste

private void populateTablesWithBatch(Connection connection, int batchSize) throws SQLException {
    Faker faker = new Faker();
    String insertUsersSql = "INSERT INTO users (name, email) VALUES (?, ?)";

    // Désactivation de l'auto-commit pour gérer la transaction
    boolean initialAutoCommit = connection.getAutoCommit();
    connection.setAutoCommit(false);

    try (PreparedStatement pstmt = connection.prepareStatement(insertUsersSql)) {
        // Création et exécution des batchs
        for (int i = 0; i < batchSize; i++) {
            pstmt.setString(1, faker.name().fullName());
            pstmt.setString(2, faker.internet().emailAddress());
            pstmt.addBatch();

            // Exécution tous les 100 enregistrements
            if ((i + 1) % 100 == 0) {
                int[] results = pstmt.executeBatch();
                System.out.printf("Lot de %d insertions traité%n", results.length);
                pstmt.clearBatch();
            }
        }

        // Exécution du dernier lot partiel
        int[] remainingResults = pstmt.executeBatch();
        System.out.printf("Dernier lot de %d insertions traité%n", remainingResults.length);

        // Validation de la transaction
        connection.commit();
    } catch (SQLException e) {
        // Annulation en cas d'erreur
        System.err.println("Erreur pendant le batch insert: " + e.getMessage());
        connection.rollback();
        throw e;
    } finally {
        // Restauration de l'état initial de l'auto-commit
        connection.setAutoCommit(initialAutoCommit);
    }
}
private static final String DB_URL = "jdbc:h2:mem:testdb_batch";
private static final String DB_USERNAME = "sa";
private static final String DB_PASSWORD = "";


try (Connection connection = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)) {
    // Création de la table
    createTables(connection);

    // Mesure du temps pour l'insertion en batch
    long startTime = System.currentTimeMillis();
    populateTablesWithBatch(connection, 1000);
    long endTime = System.currentTimeMillis();
    System.out.printf("Insertion en batch: %d ms%n", endTime - startTime);

    // Vérification du nombre d'enregistrements
    try (Statement stmt = connection.createStatement();
         ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM users")) {
        if (rs.next()) {
            System.out.printf("Nombre total d'utilisateurs: %d%n", rs.getInt(1));
        }
    }
} catch (SQLException e) {
    e.printStackTrace();
}
Lot de 100 insertions traité
Lot de 100 insertions traité
Lot de 100 insertions traité
Lot de 100 insertions traité
Lot de 100 insertions traité
Lot de 100 insertions traité
Lot de 100 insertions traité
Lot de 100 insertions traité
Lot de 100 insertions traité
Lot de 100 insertions traité
Dernier lot de 0 insertions traité
Insertion en batch: 1640 ms
Nombre total d'utilisateurs: 1000