Simplifiez-vous JDBC avec Jakarta Commons DbUtils

Christophe Jollivet

Au cours de ce tutoriel, vous allez apprendre comment l'API Jakarta Commons DbUtils peut vous simplifier l'utilisation de JDBC par l'ajout d'une couche d'abstraction supplémentaire. ♪

- Titre : Simplifiez-vous JDBC avec Jakarta Commons DbUtils

- Auteur : Christophe Jollivet

- Parution : 10 décembre 2004

- Mise à jour : 10 décembre 2004

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2004 Christophe Jollivet. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Merci

- I. Introduction

- II. L'API

- II-A. DbUtils

- II-B. ResultSetHandler

- II-C. QueryLoader

- II-D. QueryRunner

- III. Un peu de pratique

- III-A. Structure de la base de données

- III-B. Le code

- III-B-1. La classe de test

- III-B-2. Le Bean

- III-B-3. Le fichier queries.properties

- III-B-4. Explications

- IV. Conclusion

Merci

Merci à Stessy pour les corrections et les suggestions, ainsi qu'à Bestiol pour le deuxième tour de corrections.

I - Introduction

L'écriture d'un bon code JDBC n'est pas très difficile. Mais cette tâche peut vite devenir répétitive et pénible. De plus les erreurs sont souvent difficiles à retrouver.
L'API DbUtils de Jakarta Commons est simple et facile d'usage. Elle ne fait pas grand-chose de compliqué, mais simplifie beaucoup la tâche du développeur en encapsulant l'usage de JDBC. Bien qu'il existe de nombreux frameworks de persistance qui rendent la gestion de JDBC transparente, leur mise en place et configuration plutôt complexes font souvent qu'ils ne sont pas utilisés sur des projets de plus faible envergure. L'objectif de DbUtils n'est pas le remplacement des frameworks de persistance, mais la simplification de l'utilisation de JDBC. La gestion de la persistance des objets reste la tâche du programmeur.
DbUtils est disponible en téléchargement à l'adresse suivante : http://jakarta.apache.org/commons/dbutils/
Cette API ne dépend d'aucune autre API de Jakarta Commons et ne nécessite qu'un JDK version 1.2 ou plus et JDBC 2.0 ou plus. La documentation n'est pas la plus fournie, mais reste néanmoins suffisante une fois que l'on a compris le principe.
L'objectif de ce tutoriel est de présenter sommairement cette API ainsi que ses avantages au travers d'un rapide descriptif et d'exemples.

Si vous voulez savoir ce que cette API peut vous apporter, comparez les deux fonctions ci-dessous. Toutes les deux effectuent une connexion avec une base de données MySQL à partir d'un objet DataSource reçu en paramètre et renvoie une List de Bean du type EleveBean. Ce bean conprend trois attributs : nom, age et classe.

Sans DBUtils

public List fonctionClassique(DataSource ds) {
Connection conn = null;
Statement s = null;
ResultSet rs = null;
String requete = "Select nom, age, classe from Eleve where age='12'";
List lBeans = new ArrayList();
try {
//Etablissement de la connexion
conn = ds.getConnection();
s = conn.createStatement();
//Exécution de la requête
rs = s.executeQuery(requete);
//Traitement du résultat
while (rs.next()) {
EleveBean bean = new EleveBean();
bean.setNom(rs.getString("nom"));
bean.setAge(rs.getInt("age"));
bean.setClasse(rs.getString("classe"));
lBeans.add(bean);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
//fermeture des différents éléments
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
}
rs = null;
}
if (s != null) {
try {
s.close();
} catch (SQLException e) {
}
s = null;
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
}
conn = null;
}
}
return lBeans;
}

avec DBUtils

public List fonctionDbUtils(DataSource ds) {
String requete = "Select nom, age, classe from Eleve where age='12'";
//préparation d'un gestionnaire de requête
QueryRunner qRunner = new QueryRunner(ds);
//Préparation de la liste pour la réponse
List lBeans = new ArrayList();
//Execution de la requête et remplissage de la List
try {
Beans = (List) qRunner.query(requete, new BeanListHandler(EleveBean.class));
} catch (SQLException e) {
e.printStackTrace();
}
return lBeans;
}

II - L'API

L'API est constituée de trois packages :

- org.apache.commons.dbutils : contient la définition des interfaces, les classes pour l'interaction avec la base de données et les classes basiques pour l'exploitation des résultats ;

- org.apache.commons.dbutils.handlers : contient les classes de manipulation du ResultSet. Elles permettent le transfert des informations contenues dans le ResultSet vers des tableaux, des List de Bean ou des List de Map ;

- org.apache.commons.dbutils.wrappers : contient des classes permettant « d'emballer » les ResultSet avec soit la fonction de trim() automatique des String, soit une gestion des NULL.

Dans ce tutoriel nous allons voir trois classes (org.apache.commons.dbutils.DbUtils, org.apache.commons.dbutils.QueryRunner et org.apache.commons.dbutils.QueryLoader) et une interface (org.apache.commons.dbutils.ResultSetHandler). Puis nous verrons un exemple d'utilisation.

II-A - DbUtils

Toutes les méthodes de cette classe sont statiques. Ce sont des méthodes utilitaires qui permettent de faire toutes les tâches de routines s'articulant autour de l'utilisation d'une base de données (chargement de driver JDBC, ouverture et fermeture de connexions). Nous allons voir quelques-unes de ces méthodes :

- loadDriver(String driverClassName) : cette méthode charge et enregistre le driver JDBC et retourne TRUE en cas de succès. L'utilisation de cette méthode rend le code plus lisible puisqu'elle dispense de la gestion de l'exception ClassNotFoundException. De plus on dispose alors d'un booléen pour savoir si le chargement s'est bien effectué ;

- close() : cette méthode peut accepter un Connection, un Statement ou un ResultSet. Elle vérifie si l'argument fourni est null, et s'il ne l'est pas, elle le ferme. Elle est susceptible de renvoyer une SQLException ;

- closeQuietly() : elle a la même fonction que la précédente, mais masque l'éventuelle SQLException qui peut survenir. Elle présente en plus une version acceptant les trois arguments en simultané : closeQuietly(Connection conn, Statement stmt, ResultSet rs). Ce qui permet de réduire à une ligne la fermeture des trois objets se faisant généralement dans les blocs finally ;

- commitAndCloseQuietly(Connection conn) : cette méthode effectue un « commit » sur la connexion et la ferme sans remonter aucune des Exceptions pouvant survenir.

II-B - ResultSetHandler

L'implémentation de cette interface par une classe lui permet de manipuler des ResultSet et convertir les données qu'ils contiennent dans une autre forme. L'API fournit huit implémentations de cette interface permettant d'obtenir au choix : un tableau, un Bean, un Map, un List des précédents objets ou une gestion du ResultSet par colonne (une colonne dans un objet ou un List d'objets correspondant aux colonnes).
L'interface présente une seule méthode : Object handle(java.sql.ResultSet rs). Ainsi toutes les implémentations de ResultSetHandler acceptent en paramètre un ResultSet et renvoient un java.lang.Object. Par conséquent, exception faite des types primitifs, il n'y a pas de restriction sur ce qui peut être retourné par votre implémentation de l'interface.

II-C - QueryLoader

Cette classe permet le chargement d'une Map contenant les différentes requêtes SQL à partir d'un fichier properties. Cette externalisation de vos requêtes permet d'en faciliter des éventuelles optimisations. Cette Map ne doit pas être demandée par un chemin de fichier système, mais comme une ressource du jar de l'application. Cette classe est un singleton qui s'utilise comme un registre pour vos requêtes. Les deux méthodes intéressantes de cette classe sont :

- instance() : cette méthode statique permet de récupérer le QueryLoader ;

- load(java.lang.String path) : cette méthode renvoie la Map de requête qui est chargée à partir du fichier properties situé à l'adresse path. Ainsi pour le fichier queries.properties qui se trouve dans le package « mon.package.jdbc », le chemin est « /mon/package/jdbc/queries.properties ».

II-D - QueryRunner

Cette classe simplifie l'exécution de la requête. En association avec un ResultSetHandler, elle permet de réduire considérablement le code nécessaire. Cette classe dispose de deux constructeurs. Le premier est un constructeur sans argument, le second prend un javax.sql.DataSource en paramètre. Si vous ne fournissez pas de Connection comme argument à une méthode, le DataSource fourni au constructeur est utilisé pour l'obtention d'une nouvelle connexion. Le DataSource peut aussi être paramétré avec la méthode setDataSource().
Les méthodes importantes de cette classe sont :

- query(Connection conn, String requête, ResultSetHandler rsh) : cette méthode exécute une requête de type SELECT. La méthode gère la création et la fermeture du PreparedStatement et du ResultSet. Le ResultSetHandler est responsable de la conversion du ResultSet en objet plus simple d'utilisation pour le reste de l'application. Il existe une méthode sans paramètre Connection qui utilise alors le DataSource pour l'obtention de la connexion à la base de données ;

- query(Connection conn, String sql, Object[] params, ResultSetHandler rsh) : c'est le même type de méthodes, mais gérant une requête où les valeurs du tableau d'Object sont utilisées comme paramètres de la requête. De même, il existe une version sans le paramètre Connection ;

- update(Connection conn, String sql, Objet[] params) : cette méthode est utilisée pour exécuter des requêtes de type INSERT, UPDATE ou DELETE. Le tableau d'Object contient les paramètres de remplacement pour la requête.

III - Un peu de pratique

Pour la mise en pratique, nous allons réaliser une application en mode console, avec une base de données MySQL. 4.0.15, un pilote JDBC : mysql-connector-java-3.0.15.
Dans ce programme, nous allons effectuer trois tests de récupération de données :

- le premier renvoie toute la table dans un tableau ;

- le second utilise une requête avec paramètres ;

- le troisième renvoie une ArrayList contenant des Bean représentant les tuples du ResultSet.

Les requêtes des deuxième et troisième tests seront dans un fichier properties.

III-A - Structure de la base de données

Sur la base de données MySQL, le tutoriel utilise une base appelée « base » avec un user « user » et un password « password ». La base contient une table « eleve » avec trois champs

nom (varchar)

age (integer)

classe (varchar)

Dupont

12

5B

Durand

11

6A

Dugenoux

14

4C

Dupond

12

5B

III-B - Le code

III-B-1 - La classe de test

package tuto.dbutils;

import java.io.IOException;
import java.sql.*;
import java.util.*;
import org.apache.commons.dbutils.*;
import org.apache.commons.dbutils.handlers.*;

public class DbUtilsUse {

public static void main(String[] args) {
Connection conn = null;
String jdbcURL = "jdbc:mysql://localhost:3306/base";
String jdbcDriver = "org.gjt.mm.mysql.Driver";

//chargement du Driver
boolean connect;
connect=DbUtils.loadDriver(jdbcDriver);
if(!connect){
//pas de Driver on s'arrête
System.out.println("Driver absent.");
System.exit(0) ;
}
try {
//établissement de la connexion au SGBD
conn = DriverManager.getConnection(jdbcURL, "user", "password");
} catch (SQLException e1) {
e1.printStackTrace();
}
//préparation d'un gestionnaire de requête
QueryRunner qRunner = new QueryRunner();

//utilisation simple avec un ArrayList
List list1 = new ArrayList();
try {
list1 =
(List) qRunner.query(
conn,
"select * from eleve",
new ArrayListHandler());
} catch (SQLException e2) {
e2.printStackTrace();
}
//affichage du résultat
System.out.println("en ArrayList");
for (int i = 0; i < list1.size(); i++) {
Object data[] = (Object[]) list1.get(i);
System.out.println("\t"+data[0]+" -> "+data[1]+" -> "+data[2]);
}

//chargement d'un Map de requêtes
QueryLoader qLoad = QueryLoader.instance();
Map mapQuery = new HashMap();
try {
mapQuery = qLoad.load("/tuto/dbUtils/queries.properties");
System.out.println("map chargée");
} catch (IOException e) {
e.printStackTrace();
}

//utilisation d'une requête à paramètre depuis le mapQuery
//utilisation d'un ArrayHandler car il n'y a qu'un résultat
Object reponse[] = new Object[3];
String param="14";
try {
reponse =
(Object[]) qRunner.query(
conn,
(String) mapQuery.get("param"),
param,
new ArrayHandler());
} catch (SQLException e3) {
e3.printStackTrace();
}
//affichage du résultat.
System.out.println("Requete avec parametre");
System.out.println("\t"+reponse[0]+" -> "+reponse[1]+" -> "+reponse[2]);

//utilisation d'un Bean
List lBeans= new ArrayList();
try {
lBeans =
(List) qRunner.query(
conn,
(String) mapQuery.get("duplike"),
new BeanListHandler(EleveBean.class));
} catch (SQLException e4) {
e4.printStackTrace();
}
//affichage du résultat
System.out.println("requete avec Bean");
for (int i = 0; i < lBeans.size(); i++) {
EleveBean eleve = (EleveBean) lBeans.get(i);
System.out.println("\t"+eleve.getNom()+" -> "+eleve.getAge());

}
//fermeture silencieuse de la connexion
DbUtils.closeQuietly(conn);
}
}

III-B-2 - Le Bean

Ce Bean est destiné à recevoir les informations de la base de données.

package tuto.dbutils;

public class EleveBean {

private String nom;
private int age;
private String classe;

public EleveBean(){
}
public String getNom() {
return nom;
}
public int getAge() {
return age;
}
public String getClasse(){
return classe;
}
public void setNom(String s) {
nom = s;
}
public void setAge(int i) {
age = i;
}
public void setClasse(String s){
classe = s;
}
}

III-B-3 - Le fichier queries.properties

Il s'agit d'un simple fichier texte. Il contient deux requêtes. La première est une requête simple, la seconde accepte un paramètre.

duplike=select * from eleve where nom like 'dup%';
param=select * from eleve where age=?;

III-B-4 - Explications

Dans ce programme de test, nous commençons par utiliser la classe DbUtils pour établir la connexion à la base de données. Cette connexion sera ensuite passée en paramètre pour l'exécution des requêtes. Nous créons aussi une instance de QueryRunner pour l'exécution des requêtes. Je rappelle que si vous travaillez avec un pool de connexions, il est possible de passer directement le DataSource en paramètre lors de la construction de QueryRunner. L'utilisation de DbUtils n'est alors plus nécessaire.

Pour le premier test, nous commençons par une requête simple de toute la base avec un ArrayListHandler. Elle renvoie une ArrayList (list1)dont chaque élément est un tableau d'Object (données[]) qui correspond chacun à un tuple. Chaque Object du tableau est une donnée de la base. Dans notre exemple, le cast de ces Object n'est pas nécessaire pour l'affichage, mais il peut le devenir selon votre utilisation.

Au début du second test, nous chargeons la Map avec les requêtes. Nous effectuons alors une requête avec un paramètre et utilisons un Arrayhandler, le paramètre étant la clé primaire de la table, la réponse est constituée d'un tuple maximum. Ce tuple nous est retourné sous la forme d'un tableau d'Object. Ce tableau d'Object est ensuite affiché.

Pour le troisième test, nous utilisons encore une requête de la Map, mais avec un Bean pour recevoir le résultat. L'utilisation du BeanListHandler simplifie beaucoup celle d'une base de données puisque la commande nous renvoie directement une ArrayList de Bean correspondant chacun à un tuple de la base de données.

L'utilisation du BeanListHandler amène toutefois quelques contraintes. En effet, il faut que les noms et types de colonnes de votre ResultSet correspondent aux différents champs de votre Bean. Pour cela, il est possible d'utiliser des alias pour les titres de vos colonnes dans votre requête du genre : « Select colonne1 as value1, colonne2 as value2 from table ». Vous n'êtes pas obligé d'aller chercher la valeur de tous les attributs de votre Bean dans la base de données.

En cas d'incompatibilité, il vous reste la possibilité d'écrire votre propre ResultSetHandler se chargeant de la conversion des champs posant problème (champs Date, int stocké dans des champs varchar…)

Pour finir, la fin du programme de test ferme la connexion à la base de données de façon silencieuse, c'est-à-dire en empêchant une éventuelle Exception de remonter.

IV - Conclusion

Dans ce tutoriel, nous avons vu que l'utilisation de l'API jakarta.commons.DbUtils permet de se simplifier beaucoup l'accès à une base de données. En effet il est possible d'externaliser vos requêtes vers un fichier properties pour en faciliter la modification. Il est aussi possible d'éviter l'écriture de bloc try-catch en faisant passer sous silence certaines exceptions sans incidence (lors de la fermeture de la connexion). Enfin lors de l'utilisation avec un pool de connexions, il est possible d'obtenir une ArrayList de Bean à partir de la base de données avec seulement deux instructions.