Skip to content

6. [Cours] : Introduction à l'API JDBC

Mots clés : bases de données relationnelles, API JDBC, SQLException.

6.1. Support

Le dossier [support / chap-06] contient les projets Eclipse de ce chapitre.

6.2. Architecture

La couche JDBC (Java DataBase Connectivity) est une interface d'accès universelle aux bases de données. Elle présente toujours la même interface à la couche [DAO]. Si on change de SGBD, il suffit de changer le pilote JDBC. La couche [DAO] ne change pas.

6.3. Les étapes d'exploitation d'une base de données

Dans l'architecture ci-dessus, l'exploitation d'une base de données par le programme console comporte les étapes suivantes :

  1. chargement du pilote JDBC de la base de données ;
  2. ouverture d'une connexion avec la base ;
  3. émission d'un ordre SQL sur la base et traitement des résultats de l'ordre SQL ;
  4. fermeture de la connexion ;

L'étape 1 ne se fait qu'une fois. Les étapes 2-4 se font de façon répétée. On notera qu'on ne laisse pas une connexion ouverte. On la ferme dès qu'on en n'a plus besoin.

6.3.1. étape 1 - chargement en mémoire du pilote JDBC

Le code


        // chargement du pilote JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
            // traiter l'exception
}

L'opération de la ligne 3 a pour but de charger en mémoire le pilote JDBC de la base de données. Cette opération n'a besoin d'être faite qu'une fois. La répéter ne cause cependant pas d'erreur. La classe du pilote JDBC est cherchée dans le Classpath du projet. Il faut donc que dans le projet Eclipse, le [jar] contenant la classe du pilote JDBC ait été inclus dans le Classpath du projet.

6.3.2. étape 2 - ouverture d'une connexion

Une fois le pilote JDBC en place, on lui demande d'ouvrir une connexion avec la BD :

Le code


package spring.jdbc;

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

public class IntroJdbc01 {

...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // on traite l'exception
            ...
        } finally {
         // fermer la connexion
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // traiter l'exception
                ...
            }
         }
}
  • lignes 3-7 : les classes d'implémentation de l'interface JDBC sont toutes dans le package [java.sql]. Par ailleurs, en cas d'erreur elles lancent toutes une exception de type [SQLException] (ligne 19, 27). Cette exception dérive de la classe [Exception] et est une exception dite contrôlée : on est obligé de mettre un try / catch pour la gérer ou de façon alternative de ne pas la gérer et d'indiquer que la méthode laisse sortir l'exception en complétant la signature de la méthode par [throws SQLException] ;
  • ligne 17, [DriverManager.getConnection] est une méthode statique qui attend trois paramètres :
    • [url] : l'URL de la base de données. C'est une chaîne de caractères dépendante de la BD utilisée. Pour MySQL, elle est de la forme [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user] : le propriétaire de la connexion ;
    • [passwd] : son mot de passe ;
  • lignes 24-30 : la connexion doit être fermée dans la clause [finally] afin qu'elle soit fermée qu'il y ait exception ou non.

6.3.3. étape 3 - émission d'ordres SQL [SELECT]

Une fois obtenue une connexion, on peut émettre des ordres SQL. La façon de gérer des ordres de lecture [SELECT] diffère de celle utilisée pour les opérations de mise à jour [UPDATE, INSERT, DELETE]. Nous commençons par les ordres SQL [SELECT] :

Le code


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // en mode lecture seule
            connexion.setReadOnly(true);
            // on lit la table [PRODUITS]
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // on traite l'exception
             doCatchException(connexion,e1);
        } finally {
            // on traite le finally
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • lignes 8, 10 : ouverture d'une transaction (ligne 8) en mode lecture seule (ligne 10). Une transaction est une séquence d'ordes SQL qui soit tous réussissent soit tous échouent. Ainsi dans une transaction comportant N ordres SQL, si l'ordre I+1 échoue, alors les I précédents seront annulés. Pour une opération de lecture, une transaction n'est pas nécessaire. Néanmoins créer une transaction en lecture seule peut permettre à certains SGBD de faire certaines optimisations ;
  • ligne 12 : utilisation d'un [PreparedStatement]. Un [PreparedStatement] a normalement des paramètres notés par le caractère ?. Ici il n'en a pas. Un [PreparedStatement] est un ordre préparé par le SGBD. Cette préparation a un coût et elle n'est faite qu'une fois. Ensuite cet ordre préparé est exécuté par le SGBD avec différents paramètres effectifs qui vont venir remplacer les paramètres formels ?. A noter qu'il est préférable de nommer les colonnes désirées plutôt que d'utiliser la notation * pour obtenir toutes les colonnes. En précisant le nom des colonnes on peut ensuite obtenir leurs valeurs à partir de leur position dans la requête SELECT ;
  • ligne 13 : exécution du [PreparedStatement]. On récupère un objet de type [ResultSet] ;

Un objet de type [ResultSet] représente une table, c’est à dire un ensemble de lignes et de colonnes. A un moment donné, on n’a accès qu’à une ligne de la table appelée ligne courante. Lors de la création initiale du [ResultSet], il n'y a pas de ligne courante. Il faut faire une opération [ResultSet.next()] pour l'obtenir. La signature de la méthode next est la suivante :

    boolean next()

Cette méthode tente de passer à la ligne suivante du [ResultSet] et rend true si elle réussit, false sinon. En cas de réussite, la ligne suivante devient la nouvelle ligne courante. La ligne précédente est perdue et on ne pourra revenir en arrière pour la récupérer.

La table du [ResultSet] a des colonnes nommées labelCol1, labelCol2,... précisées dans la requête [SELECT] exécutée. Avec la requête :

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • la colonne [ID] ira dans une colonne du [ResultSet] nommée [myId] ;
  • la colonne [NOM] ira dans une colonne du [ResultSet] nommée [myNom] ;
  • ...

Ci-dessus, les identifiants [myCol] sont appelés des labels de colonne. En l'absence de ces labels, les noms des colonnes du [ResultSet] sont dépendants du SGBD. Lorsque le [SELECT] opère sur une unique table, les labels des colonnes seront par défaut les noms des colonnes demandées par le SELECT. Le problème surgit lorsque le [SELECT] opère sur plusieurs tables et que dans celles-ci on trouve des noms de colonnes identiques comme dans l'exemple suivant :

SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

en imaginant que la table [PRODUITS] ait une clé étrangère vers la table [CATEGORIES] symbolisée par la relation [Produits].CATEGORIE_ID --> [CATEGORIES].ID, et que les tables [PRODUITS] et [CATEGORIES] aient toutes les deux un champ [NOM]. Dans ce cas, les noms donnés dans le [ResultSet] aux colonnes [PRODUITS.NOM] et [CATEGORIES.NOM] sont dépendants du SGBD. Pour la portabilité entre SGBD, il faut donc utiliser des labels de colonnes ici et on écrira :


SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

Pour exploiter les différents champs de la ligne courante du [ResultSet], on dispose des méthodes suivantes :

Type getType("labelColi") 

pour obtenir la colonne nommée «labelColi» de la ligne courante et donc la colonne du [SELECT] ayant ce label. Type désigne le type du champ coli. On peut utiliser les méthodes [getType] suivantes : getInt, getLong, getString, getDouble, getFloat, getDate, ... Au lieu d'utiliser le nom de la colonne, on peut utiliser sa position dans la requête [SELECT] exécutée :

Type getType(i) 

où i est l’indice de la colonne désirée (i>=1).

  • lignes 15-17 : récupération des valeurs lues dans la BD ;
  • ligne 19 : la transaction est validée (on dit également committée). Cela la termine et libère les ressources que le SGBD avait mobilisées pour elle ;
  • ligne 25 : les ressources sont libérées dans le [finally]. Celui-ci appelle la méthode [doFinally] suivante :

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // fermeture ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // fermeture [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // fermer la connexion
                connexion.close();
            } catch (SQLException e3) {
                // traiter l'exception
            }
        }
    }
  • lignes 3-9 : fermeture du [ResultSet] ;
  • lignes 11-17 : fermeture du [PreparedStatement] ;
  • lignes 18-27 : fermeture de la connexion ;

Les fermetures des lignes 3-17 semblent redondantes dans la mesure on ferme la connexion lignes 18-25. En fait, dans certains cas elles ne le sont pas et il est conseillé de les laisser [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection].

  • ligne 22 : l'exception est traitée par la méthode [doCatchException] suivante :

    private static void doCatchException(Connection connexion, Throwable th) {
        // annulation transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // traiter l'exception
        }
}
  • lignes 4-6 : la transaction est annulée. Cela la termine et le SGBD va pouvoir relâcher les ressources mobilisées pour elle ;

6.3.4. étape 3 - émission d'ordres SQL [INSERT, UPDATE, DELETE]

Les ordres SQL [INSERT, UPDATE, DELETE] sont des opérations de mise à jour : elles modifient la base de données mais ne ramènent aucune ligne. La seule information rendue est le nombre de lignes affectées par l'opération de mise à jour.

Le code


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // en mode lecture / écriture
            connexion.setReadOnly(false);
            // on met à jour la table
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // catégorie 1
            ps.setInt(1, 10);
            // exécution
            int nbLignes=ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException(connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, ps, connexion);
        }
    }
  • ligne 9 : la connexion est utilisée en lecture et écriture ;
  • ligne 11 : un [PreparedStatement] avec 1 paramètre (symbolisé par ?). On peut avoir plusieurs paramètres. Ils sont numérotés à partir de 1 ;
  • ligne 13 : on affecte sa valeur à l'unique paramètre. Le 1er paramètre de [setType] est la position du paramètre dans le [PreparedStatement] (1, 2, ...) et le second la valeur qui lui est attribuée. On peut utiliser les méthodes [setInt, setLong, setFloat, setDouble, setString, setDate, ...] ;
  • ligne 15 : on utilise la méthode [executeUpdate] et non [executeQuery] réservée aux ordres SELECT. La méthode rend le nombre de lignes affectées par l'opération. Peut être 0.
  • ligne 17 : la transaction est validée ;

6.3.5. étape 4 - fermeture de la connexion

Une connexion doit être fermée le plus vite possible dans un contexte multi-utilisateurs car un SGBD accepte un nombre limité de connexions ouvertes. Dans les exemples précédents, elle était fermée dans la clause [finally] des opérations SQL afin qu'elle soit fermée qu'il y ait eu exception ou pas.

6.4. Un projet exemple

6.4.1. Support

Le dossier [support / chap5] contient les projets Eclipse de ce chapitre [1, 2]. Le dossier [database] contient le script SQL permettant de créer la base MySQL exemple de ce chapitre [1, 3].

6.4.2. La base de données exploitée

Les exemples qui suivent exploitent la base de données MySQL suivante :

 
  • [ID] : clé primaire en mode AUTO_INCREMENT (si on ne donne pas de clé primaire, le SGBD la génère) ;
  • [NOM] : nom d'un produit - unique ;
  • [CATEGORIE] : n° de sa catégorie ;
  • [PRIX] : son prix ;
  • [DESCRIPTION] : une description du produit ;

On la créera avec l'outil [WampServer] de la façon suivante [1-9] :

6.4.3. Le projet Eclipse

  

Le projet est un projet Maven défini par le fichier [pom.xml] suivant :


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
    </dependencies>
</project>
  • lignes 8-12 : le pilote JDBC du SGBD MySQL5 ;
  • lignes 13-17 : une bibliothèque capable de gérer du jSON (Javascript Object Notation) (cf paragraphe 22.6). Nous allons l'utiliser pour afficher sous forme jSON les produits de la base de données ;

6.4.4. La classe des produits

La classe [Produit] est la suivante :


package istia.st.jdbc;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Produit {

    // champs
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // constructeurs
    public Produit() {

    }

    public Produit(int id, String nom, int categorie, double prix, String description) {
        this.id = id;
        this.nom = nom;
        this.categorie = categorie;
        this.prix = prix;
        this.description = description;
    }

    // getters et setters
    ...

    // to String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • ligne 34 : on utilise la bibliothèque jSON pour afficher la chaîne jSON du produit. Cela donne un affichage analogue au suivant :
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}

L'intérêt de la méthode [toString] ci-dessus est que si on ajoute / retranche des champs à la classe, sa méthode [toString] est toujours valide. Par ailleurs, si les champs sont eux-mêmes des objets (listes, tableaux, dictionnaires, objets utilisateur), les bibliothèques jSON savent les transformer à leur tour en chaîne jSON ;

6.4.5. La classe [Static]

La classe [Static] regroupe dans des méthodes du code utilisé fréquemment dans la classe principale :


package istia.st.jdbc;

import java.util.ArrayList;
import java.util.List;

public class Static {

    public static List<String> getErreursFromThrowable(Throwable th) {
        // on récupère la liste des msg d'erreur de l'exception
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // message d'erreur du throwable
            erreurs.add(th.getMessage());
            // on passe à la cause du throwable
            th = th.getCause();
        }
        // résultat
        return erreurs;
    }
    
    public static void show(String title, List<String> messages){
        // titre
        System.out.println(String.format("%s : ",title));
        // messages
        for(String message : messages){
            System.out.println(String.format("- %s",message));
        }
    }
}
  • lignes 8-19 : permet d'avoir la liste des erreurs encapsulées dans un objet de type [Throwable] qui est la classe mère de la classe [Exception] ;
  • lignes 21-28 : affiche à l'écran une liste de messages ;

Ce code pourrait être dans la classe principale parce qu'ici c'est la seule à l'utiliser. On se place ici dans un cas plus large ou d'autres classes auraient besoin de ce code.

6.4.6. Le squelette de la classe principale


package istia.st.jdbc;

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

public class IntroJdbc01 {

    // constantes
    final static String url = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String user = "root";
    final static String passwd = "";
    final static String insert = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String delete = "DELETE FROM PRODUITS";
    final static String select = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String update = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String insert2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

    public static void main(String[] args) {
        // chargement du pilote JDBC de MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // on vide la table [PRODUITS]
        delete();
        // on la remplit
        insert();
        // on la lit
        select();
        // mise à jour
        update();
        // affichage
        select();
        // insertion de deux éléments identiques
        // l'insertion doit échouer et aucun des deux éléments n'est inséré à cause de la transaction
        insert2();
        // on vérifie
        select();
        // fini
        System.out.println("Travail terminé");
    }

    // liste des produits
    private static void select() {
...
    }

    // suppression produits
    public static void delete() {
...
    }

    // ajout produits
    public static void insert() {
...
    }

    // ajout 2 produits
    public static void insert2() {
...
    }

    // mise à jour de certains produits
    public static void update() {
..
    }

    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // fermeture ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // fermeture [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // fermer la connexion
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // on affiche les msg d'erreur
                Static.show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        Static.getErreursFromThrowable(e3));
            }
        }
    }

    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // on affiche les msg d'erreur
        Static.show(title, Static.getErreursFromThrowable(th));
        // annulation transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // on affiche les msg d'erreur
            Static.show(title, Static.getErreursFromThrowable(e2));
        }
    }
}

6.4.7. Suppression du contenu de la table des produits

La méthode [delete] supprime le contenu de la table :


// suppression produits
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // on vide la table [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // retour au mode par défaut
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, null, connexion);
        }
    }

Cet exemple utilise des transactions. Une transaction permet de regrouper des ordres SQL qui doivent être tous réussis ou tous annulés. Il y a quatre opérations à connaître :

  • début d'une transaction : [connexion.setAutoCommit(false)] ;
  • fin d'une transaction avec succès : [connexion.commit()]. Dans ce cas, toutes les opérations faites sur la BD lors de la transaction sont validées ;
  • fin d'une transaction avec échec  : [connexion.rollback()]. Dans ce cas, toutes les opérations faites sur la BD lors de la transaction sont annulées ;
  • retour au mode [auto-commit] qui est le mode par défaut de l'API JDBC : [connexion.setAutoCommit(true)]. Dans ce mode, chaque ordre SQL fait l'objet d'une transaction. Ainsi si on fait deux insertions dont la deuxième échoue :
    • en mode [AutoCommit=true], la première insertion reste (elle a été validée par le 1er AutoCommit) ;
    • en mode [AutoCommit=false], la première insertion est annulée ;

Dans nos exemples, à chaque fois que se produit une exception, nous annulons la transaction dans la méthode [doCatchException] :


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // on affiche les msg d'erreur
        Static.show(title, Static.getErreursFromThrowable(th));
        // annulation transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // on affiche les msg d'erreur
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

6.4.8. Création du contenu de la table des produits

La méthode [insert] crée le contenu de la table :


// ajout produits
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // on remplit la table
            ps = connexion.prepareStatement(insert);
            for (int i = 0; i < 10; i++) {
                // préparation
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // exécution
                ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // retour au mode par défaut
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, null, connexion);
        }
    }

6.4.9. Affichage du contenu de la table des produits

La méthode [select] affiche le contenu de la table :


    // liste des produits
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // on lit la table [PRODUITS]
            ps = connexion.prepareStatement(select);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // commit transaction
            connexion.commit();
            // retour au mode par défaut
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, null, connexion);
        }
}

6.4.10. Mise à jour du contenu de la table

La méthode [update] met à jour certains produits :


// mise à jour de certains produits
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // on met à jour la table
            ps = connexion.prepareStatement(update);
            // catégorie 1
            ps.setInt(1, 1);
            // exécution
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // retour au mode par défaut
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, null, connexion);
        }
    }

6.4.11. Rôle de la transaction

La méthode [insert2] insère deux produits de même clé primaire dans la table, ce qui n'est pas possible. Comme on est dans une transaction, le première insertion va être annulée.


// ajout de  2 produits de mêmes clés primaires
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // on ajoute 1 ligne
            ps = connexion.prepareStatement(insert2);
            // exécution
            ps.executeUpdate();
            // on ajoute la même ligne une 2ème fois donc avec la même clé primaire
            // l'insertion doit échouer et aucun des deux éléments ne doit être inséré à cause de la transaction
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // retour au mode par défaut
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, null, connexion);
        }
    }

6.4.12. Résultats

Les résultats de l'exécution de la méthode [main] donne les résultats suivants :

Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Les erreurs suivantes se sont produites lors de l'ajout : 
- Duplicate entry '100' for key 'PRIMARY'
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Travail terminé

6.5. Utilisation d'une source de données de type [DataSource]

Nous allons reprendre l'application précédente en utilisant une source de données de type [javax.sql.DataSource] :

Image

Nous allons utiliser une source de données implémentée par la classe [org.apache.tomcat.jdbc.pool.DataSource]. Cette classe utilise un pool de connexions ç-à-d un ensemble de connexions ouvertes :

  • lorsque le pool est instancié, un certain nombre de connexions est ouvert avec la base de données. Ce nombre est configurable ;
  • lorsque le code Java ouvre une connexion, celle-ci est fournie par le pool ;
  • lorsque le code Java ferme une connexion, celle-ci est rendue au pool ;

Au final, les connexions ne sont ouvertes qu'une fois, ce qui améliore la performance d'accès à la base de données. La source de données sera définie dans une classe de configuration Spring

6.5.1. Le projet Eclipse

  

Le projet est un projet Maven défini par le fichier [pom.xml] suivant :


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <!-- bibliothèque jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.3.RELEASE</version>
        </dependency>
        <!-- Tomcat Jdbc -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>8.0.20</version>
        </dependency>
    </dependencies>
</project>
  • lignes 21-25 : dépendance sur Spring ;
  • lignes 27-31 : dépendance sur la bibliothèque qui fournit la source de données ;

La classe de configuration de Spring [AppConfig] est la suivante :


package istia.st.jdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    // constantes
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String INSERT = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String DELETE = "DELETE FROM PRODUITS";
    final static String SELECT = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String UPDATE = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String INSERT2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    
    @Bean
    public DataSource dataSource() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }
}
  • lignes 11-19 : les constantes précédemment définies dans [IntroJdbc01] ont migré dans [AppConfig] ;
  • lignes 31-34 : le bean Spring définissant la source de données ;
  • ligne 24 : création de la source de données encore non configurée ;
  • lignes 26-29 : les informations qui permettent à la source de données de se connecter à la base de données ;
  • ligne 31 ; crée un pool de 1 connexion. On n'en a pas besoin de plus ici. Il n'y a jamais plusieurs connexions simultanées ;

6.5.2. La classe principale

La classe principale [IntroJdbc02] est la suivante :


package istia.st.jdbc;

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

import javax.sql.DataSource;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class IntroJdbc02 {
    // source de données
    private static DataSource dataSource;

    public static void main(String[] args) {
        // récupération du contexte Spring
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // récupération de la source de données
        dataSource = ctx.getBean(DataSource.class);
        // on vide la table [PRODUITS]
        delete();
...
        // fini
        ctx.close();
        System.out.println("Travail terminé");
    }

    // liste des produits
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // ouverture connexion
            connexion = dataSource.getConnection();
            // début transaction
            connexion.setAutoCommit(false);
...
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, null, connexion);
        }
    }
...
}
  • ligne 14 : la source de données. On notera qu'elle est de type [javax.sql.DataSource] qui est une interface ;
  • ligne 18 : instanciation des objets Spring ;
  • ligne 20 : obtention d'une référence sur la source de données. On notera qu'à aucun moment on ne cite la classe réellement utilisée. Ainsi ici, rien ne laisse supposer qu'on utilise une implémentation [TomcatJdbc] ;
  • ligne 36 : obtention d'une connexion ouverte ;
  • le reste du code est identique à celui de la classe [IntroJdbc01] ;

6.6. Conclusion

On trouvera davantage d'informations sur la gestion des bases de données dans le document [Exploiter une base relationnelle avec l'écosystème Spring].