mika-el
3/30/2018 - 1:47 PM

JDBC

Réutilisation des connexions

Afin d'alléger la charge que le SGBD doit actuellement supporter à chaque requête, nous allons utiliser une technique très simple : nous allons précharger un certain nombre de connexions à la base de données, et les réutiliser. L'expression employée pour nommer cette pratique est le « connection pooling »
Le pool de connexions va pré-initialiser un nombre donné de connexions au SGBD lorsque l'application démarre. Autrement dit, il va créer plusieurs objets Connection et les garder ouverts et bien au chaud.
Ensuite, le pool de connexions va se charger de distribuer ses objets Connection aux méthodes de l'application qui en ont besoin. Concrètement, cela signifie que ces méthodes ne vont plus faire appel à DriverManager.getConnection(), mais plutôt à quelque chose comme pool.getConnection(). Enfin, puisque l'objectif du système est de partager un nombre prédéfini de ressources, un appel à la méthode Connection.close() ne devra bien entendu pas provoquer la fermeture réelle d'une connexion ! En lieu et place, c'est tout simplement un renvoi dans le pool de l'objet Connection qui va avoir lieu. De cette manière, et seulement de cette manière, la boucle est bouclée : l'objet Connection inutilisé retourne à la source, et est alors prêt à être à nouveau distribué.

Choix d'une implémentation

Une DataSource n'est qu'une interface, il est donc nécessaire d'en écrire une implémentation. Rassurez-vous, nous n'allons pas nous occuper de cette tâche : il existe plusieurs bibliothèques, libres et gratuites, qui ont été créées par des équipes de développeurs expérimentés et validées par des années d'utilisation. Sans être exhaustif, voici une liste des solutions les plus couramment rencontrées :

  • Apache DBCP

  • BoneCP

  • c3p0

  • DBPool

  • La connexion à une base de données est une étape coûteuse en termes de temps et de performances.

  • Il est nécessaire d'initialiser un nombre prédéfini de connexions, et de les partager/distribuer/réutiliser pour chaque requête entrante : c'est le principe du pool de connexions.

  • Lorsqu'un pool de connexions est en place, un appel à la méthode connexion.close() ne ferme pas littéralement une connexion, mais la renvoie simplement au pool.

  • La méthode getConnection() étant centralisée et définie dans notre Factory, il nous est très aisé de modifier son comportement.

  • Un pool de connexions se base sur le principe d'une DataSource, objet qu'il est vivement recommandé d'utiliser en lieu et place du DriverManager.

  • BoneCP est une solution de pooling très efficace, aisément configurable et intégrable à n'importe quelle application Java EE.

Les requêtes préparées

  • PreparedStatement est une nouvelle interface qui implémente l'interface Statement. Comme son nom l'indique, cet objet permet de créer des requêtes préparées.
  • PreparedStatement il présente trois différences majeures avec un Statement classique :
    • L'utilisation de PreparedStatement peut permettre de pré-compiler une requête SQL pour réduire le temps d'exécution.
    • Une requête SQL ainsi créée peut être paramétrée, c'est-à-dire contenir des trous ou jokers qui ne seront comblés que lors de son exécution.
    • Une requête SQL ainsi créée est protégée contre les injections SQL, et contre d'éventuelles erreurs sur les types des paramètres.
  • Comme pour ResultSet, il existe une méthode par type de donnée :
    • Une méthode preparedStatement.setInt() pour définir un entier.
    • Une méthode preparedStatement.setString() pour définir une chaîne de caractères.
    • Une méthode preparedStatement.setBoolean() pour définir un booléen.
    • ...
  • La plupart des méthodes de preparedStatement pour attendent simplement deux arguments :
    • Un entier définissant le paramètre à remplacer ;
    • Un objet du type concerné destiné à remplacer le paramètre dans la requête SQL.

Requête préparée de lecture

/* Création de l'objet gérant la requête préparée définie */
PreparedStatement preparedStatement = connexion.prepareStatement( "SELECT id, email, mot_de_passe, nom FROM Utilisateur WHERE email = ?;" );
/* Récuperation et assignation d'un parametre pour la requête */
preparedStatement.setString( 1, request.getParameter( "email" ));
/* Execution de la requête */
resultat = preparedStatement.executeQuery();
/* Récupération des données du résultat de la requête de lecture */
while ( resultat.next() ) {
    int idUtilisateur = resultat.getInt( "id" );
    /* Traiter ici les valeurs récupérées. */
}

Requête préparée d'écriture

/* Création de l'objet gérant les requêtes préparées */
preparedStatement = connexion.prepareStatement( "INSERT INTO Utilisateur (email, mot_de_passe, nom, date_inscription) VALUES(?, MD5(?), ?, NOW());" );

/*
 * Récupération des paramètres d'URL saisis par l'utilisateur
 * Remplissage des paramètres de la requête grâce aux méthodes
 * setXXX() mises à disposition par l'objet PreparedStatement.
 */
preparedStatement.setString( 1, request.getParameter("email"));
preparedStatement.setString( 2, request.getParameter("motdepasse"));
preparedStatement.setString( 3, request.getParameter("nom"));

/* Exécution de la requête */
int statut = preparedStatement.executeUpdate();

Exécution d'une requête d'écriture

executeUpdate() retourne un entier représentant le nombre de lignes affectées par la requête réalisée ou 0 en cas d'échec.

/* Exécution d'une requête d'écriture */
int statut = statement.executeUpdate( "INSERT INTO Utilisateur (email, mot_de_passe, nom, date_inscription) VALUES ('jmarc@mail.fr', MD5('lavieestbelle78'), 'jean-marc', NOW());" );

Retour d'une requête d'écriture

Lorsque vous effectuez une modification sur une table de votre base de données via la méthode statement.executeUpdate(), celle-ci renvoie des informations différentes selon le type de la requête effectuée :

  • L'exécution d'un INSERT renvoie 0 en cas d'échec de la requête d'insertion, et 1 en cas de succès.
  • L'exécution d'un UPDATE ou d'un DELETE renvoie le nombre de lignes respectivement mises à jour ou supprimées.
  • L'exécution d'un CREATE, ou de toute autre requête ne retournant rien, renvoie 0.
/* Exécution d'une requête d'écriture */
int statut = statement.executeUpdate( "INSERT INTO Utilisateur (email, mot_de_passe, nom, date_inscription) VALUES ('jmarc@mail.fr', MD5('lavieestbelle78'), 'jean-marc', NOW());" );

/* 
 * Exécution d'une requête d'écriture avec renvoi de l'id auto-généré 
 */
int statut = statement.executeUpdate( "..." , Statement.RETURN_GENERATED_KEYS);
// Récupération de l'id auto-généré par la requête d'insertion.
resultat = statement.getGeneratedKeys();
// Parcours du ResultSet et formatage pour affichage de la valeur qu'il contient dans la JSP finale.
while ( resultat.next() ) {
    messages.add( "ID retourné lors de la requête d'insertion :" + resultat.getInt( 1 ) );
}

/* 
 * Exécution d'une requête préparé d'écriture avec renvoi de l'id auto-généré 
 */
PreparedStatement preparedStatement = connexion.prepareStatement( "INSERT INTO Utilisateur (email, mot_de_passe, nom, date_inscription) VALUES ('jmarc@mail.fr', MD5('lavieestbelle78'), 'jean-marc', NOW());", Statement.RETURN_GENERATED_KEYS );
preparedStatement.executeQuery();
resultat = preparedStatement.getGeneratedKeys();
while ( resultat.next() ) {
    messages.add( "ID retourné lors de la requête d'insertion :" + resultat.getInt( 1 ) );
}

Exécution d'une requête de lecture

executeQuery() retourne un objet de type ResultSet contenant le résultat de la requête :

* Exécution d'une requête de lecture */
ResultSet resultat = statement.executeQuery( "SELECT id, email, mot_de_passe, nom  FROM Utilisateur;" );

Retour d'une requête de lecture

  • statement.executeQuery() retourne un objet de typeResultSet. Vous pouvez le voir comme un tableau, qui contient les éventuelles données retournées par la base de données sous forme de lignes.
  • Afin de pouvoir accéder aux lignes contenues dans cet objet, nous effectuons un appel à la méthode next(), qui permet de déplacer le curseur à la ligne suivante. Elle retourne un booléen, initialisé à true tant qu'il reste des données à parcourir.
  • Lors de la création d'un objet ResultSet, son curseur est par défaut positionné avant la première ligne de données. Ainsi, il est nécessaire de se déplacer d'un cran vers l'avant pour pouvoir commencer à lire les données contenues dans l'objet.
  • Si vous essayez de lire des données avant d'avoir déplacé le curseur, votre code enverra une SQLException.
  • ResultSet propose une méthode de récupération par type de données :
    • une méthode resultat.getInt() pour récupérer un entier
    • une méthode resultat.getString() pour récupérer une chaîne de caractères
    • une méthode resultat.getBoolean() pour récupérer un booléen
    • ...
  • Chacune de des méthodes de ResultSet existe sous deux formes différentes :
    • Soit elle prend en argument le nom du champ visé dans la table de la base de données.
    • Soit elle prend en argument l'index du champ visé dans la table de la base de données.
/* Exécution d'une requête de lecture */
ResultSet resultat = statement.executeQuery( "SELECT id, email, mot_de_passe, nom  FROM Utilisateur;" );

/* Récupération des données du résultat de la requête de lecture */
while ( resultat.next() ) {
    int idUtilisateur = resultat.getInt( "id" );
    String emailUtilisateur = resultat.getString( "email" );
    String motDePasseUtilisateur = resultat.getString( "mot_de_passe" );
    String nomUtilisateur = resultat.getString( "nom" );
    /* Traiter ici les valeurs récupérées. */
}

A propos

  • Il est nécessaire de charger le driver JDBC une seule et unique fois, au démarrage de l'application par exemple.
  • JDBC fonctionne par un système d'URL. Dans le cas de MySQL : jdbc:mysql://hôte:port/nom_de_la_bdd.
  • Il est nécessaire d'établir une connexion entre l'application et la base de données, via un appel à DriverManager.getConnection() qui retourne un objetConnection.
  • Un appel à connexion.createStatement() retourne un objetStatement, qui permet d'effectuer des requêtes via notamment ses méthodes executeQuery() et executeUpdate().
  • L'emploi de cet objet est à éviter, celui-ci étant sujet à de nombreux problèmes dont les dangeureuses injections SQL.
  • Préparer ses requêtes permet une protection contre ces injections, une gestion simplifiée des paramètres et dans certains cas de meilleures performances.
  • Un appel à connexion.prepareStatement() retourne un objet PreparedStatement, qui permet d'effectuer des requêtes de manière sécurisée, via notamment ses méthodes executeQuery() et executeUpdate().
  • Un ensemble de setters facilite l'utilisation de paramètres dans les requêtes effectuées.
  • Il est nécessaire de libérer les ressources utilisées lors d'un échange avec la base de données, en fermant les différents objets intervenant dans un ordre précis via des appels à leur méthode close().

Connexion à la base de données

  • L'établissement d'une connexion s'effectue à travers l'objetDriverManager. Il suffit d'appeler sa méthode statiquegetConnection()pour récupérer un objet de typeConnection.
  • Celle-ci prend en argument l'adresse de la base de données, le nom d'utilisateur et le mot de passe associé.
/* Connexion à la base de données */
String url            = "jdbc:mysql://nomhote:port/nombdd";
String utilisateur    = "user";
String motDePasse     = "pass";
Connection connexion  = null;

try {
    connexion = DriverManager.getConnection( url, utilisateur, motDePasse );
    /* Ici, nous placerons nos requêtes vers la BDD */
} catch ( SQLException e ) {
    /* Gérer les éventuelles erreurs ici */
} finally {
    if ( connexion != null )
        try {
            connexion.close();
        } catch ( SQLException ignore ) {
            /* Si une erreur survient lors de la fermeture, il suffit de l'ignorer. */
        }
}

Création d'une requête

  • Avant de pouvoir créer des instructions SQL, il faut d'abord créer un objet de typeStatement.
  • typeStatement est en réalité d'une interface dont le rôle est de permettre l'exécution de requêtes. Pour initialiser cet objet, il suffit d'appeler la méthode createStatement() de l'objet Connection précédemment obtenu.
/* Création de l'objet gérant les requêtes depuis l'objet connexion dans le try */
Statement statement = connexion.createStatement();
/* Avec une requête préparé */
PreparedStatement preparedStatement = connexion.prepareStatement( "..." );

Exécution de la requête

  • Une fois l'objet Statement initialisé, il devient alors possible d'exécuter une requête.
  • Pour ce faire, celui-ci met à votre disposition toute une série de méthodes, notamment les deux suivantes.
    • executeQuery(): cette méthode est dédiée à la lecture de données via une requête de type SELECT.
    • executeUpdate(): cette méthode est réservée à l'exécution de requêtes ayant un effet sur la base de données (écriture ou suppression), typiquement les requêtes de type INSERT, UPDATE, DELETE, etc.
  • En outre, il existe des variantes de chaque méthode prenant en compte d'autres arguments, ainsi que deux autres méthodes nommées execute() et executeBatch() (voir http://docs.oracle.com/javase/6/docs/api/java/sql/Statement.html).

Libération des ressources

  • Tout comme il est nécessaire de fermer proprement une connexion ouverte, il est extrêmement recommandé de disposer proprement des objets StatementetResultSet initialisés au sein d'une connexion :
  • Ceux-ci doivent obligatoirement être fermés, du plus récemment ouvert au plus ancien. Ainsi il faut commencer par fermer le ResultSet, puis le Statement et enfin l'objet Connection. Les exceptions éventuelles, envoyées en cas de ressources déjà fermées ou non disponibles, peuvent être ignorées comme c'est le cas dans ce code d'exemple. Vous pouvez bien évidemment choisir de les prendre en compte, par exemple en les enregistrant dans un fichier de logs.
  • Tout comme pour la connexion, ces ressources doivent être fermées au sein d'un blocfinallyafin de s'assurer que, quoiqu'il arrive en cours de route, les ressources soient proprement libérées.
Connection connexion = null;
Statement statement = null;
ResultSet resultat = null;
try {
    /* 
     * Ouverture de la connexion, initialisation d'un Statement, initialisation d'un ResultSet, etc.
     */
} catch ( SQLException e ) {
    /* Traiter les erreurs éventuelles ici. */
} finally {
    if ( resultat != null ) {
        try {
            /* On commence par fermer le ResultSet */
            resultat.close();
        } catch ( SQLException ignore ) {
        }
    }
    if ( statement != null ) {
        try {
            /* Puis on ferme le Statement */
            statement.close();
        } catch ( SQLException ignore ) {
        }
    }
    if ( connexion != null ) {
        try {
            /* Et enfin on ferme la connexion */
            connexion.close();
        } catch ( SQLException ignore ) {
        }
    }
}