Utilisation de JDBC pour la gestion d'images en base de données

Ricky81

Le but de ce tutoriel est de vous présenter une manière de faire pour stocker et extraire une image dans une base de données ne reposant que sur JDBC. Pour illustrer ceci, nous allons construire autour du cœur de ce tutoriel, à savoir la communication avec la base de données, une petite application permettant d'illustrer plus facilement l'interaction avec la base de données.
Ce sera donc également l'occasion de revenir sur d'autres concepts. ♪

- Titre : Utilisation de JDBC pour la gestion d'images en base de données

- Auteur : Ricky81

- Parution : 3 juin 2004

- Mise à jour : 18 novembre 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 Ricky81. 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

- Avant-propos

- Remerciements

- I. Le stockage volumineux de données en base de données

- II. Création de la table

- II-A. Base de données MySQL

- II-B. Base de données PostgreSQL

- III. Interaction avec la base de données

- III-A. Connexion/Déconnexion

- III-B. Enregistrement d'une image

- III-C. Chargement d'une image

- III-D. Liste des noms d'images

- IV. Mise en place d'une application graphique

- IV-A. JFileChooser et gestion des extensions

- IV-B. Boîte de dialogue simple avec JOptionPane

- IV-C. La classe ImageFrame

- V. Utilisation d'un jar

- V-A. Rassembler les librairies

- V-B. Création d'un manifest

- V-C. Création du jar

- V-D. Exécution du jar

- VI. Quelques liens utiles

Avant-propos

L'idée première de ce tutoriel est de vous proposer un exemple de code s'appuyant uniquement sur JDBC (en n'utilisant que des classes du paquet java.sql) permettant de réaliser les opérations de sauvegarde ou de chargement d'images sur différents SGBDR (ceux qui sont compatibles avec JDBC) sans avoir à personnaliser le code selon la base de données cible. Il est par exemple possible de réaliser une telle opération pour un SGBDR comme ORACLE, mais on est obligé de recourir à un ResultSet Oracle, ce qui empêche l'utilisation d'un code portable sur une autre base de données.

Remerciements

Je tiens avant tout à remercier la section Java, et plus particulièrement request pour ses remarques et ses conseils, ainsi que Rmotte pour les corrections orthographiques.

I - Le stockage volumineux de données en base de données

Le but n'est pas ici de débattre ou de donner une opinion concernant le stockage de données volumineuses (selon la norme SQL, le type LOB pour Large OBject) dans une base de données. Pour ceux qui ne le savent pas, des SGBDR comme ORACLE permettent par exemple de stocker des fichiers dans le système de fichier du système d'exploitation (ce qu'on appelle les BFILE chez ORACLE).

Il existe, selon les SGBDR, différentes catégories de LOB. Nous nous concentrerons ici sur les BLOB (Binary Large OBject) qui sont dédiés aux données binaires, et donc notamment aux images.

Compte tenu des objectifs rappelés en préambule, je me suis donc concentré sur les 2 SGBDR suivants pour lesquels cela est possible et que je pouvais tester : MySQL et PostgreSQL.
Nous allons dans la suite présenter comment sont géré les LOB et traiter la création d'une table pour chacune de ces bases de données.

Les versions sur lesquelles se base le contenu de ce tutoriel sont les suivantes :

- MySQL 3.23.49 (intégré dans EasyPhp) ;

- PostgreSQL 7.4.1 (utilisé sous WinXP avec Cygwin).

II - Création de la table

Après avoir créé votre base de données (ici j'ai pris « toto » et « titi » pour nom d'utilisateur / mot de passe), nous allons créer une table contenant deux colonnes : une colonne name, et une colonne img. Appelons cette table images.

II-A - Base de données MySQL

Pour MySQL, nous disposons de 4 variantes du type BLOB :

TINYBLOB

<= 255 octets (1 octet pour stocker la taille)

BLOB

<= 65 535 octets (2 octets pour la taille)

MEDIUMBLOB

<= 16 777 215 octets (3 octets pour la taille)

LONGBLOB

<= 4 294 967 295 octets (4 octets pour la taille)

Le choix du type à utiliser dépend donc du type d'informations à stocker. Pour notre exemple, nous pourrons nous contenter du type MEDIUMBLOB.

Voici l'ordre SQL de création :

CREATE TABLE image (
name varchar(20) NOT NULL,
img mediumblob,
PRIMARY KEY  (name)
);

À noter qu'il est important de bien définir la variable max_allowed_packet pour ne pas avoir d'erreurs lors du transfert de données.

II-B - Base de données PostgreSQL

Le type utilisé au niveau de PostgreSQL est BYTEA qui permet de stocker des données binaires allant jusqu'à 1 Go.

Voici l'ordre SQL de création :

CREATE TABLE "public"."image" (
"name" VARCHAR(20) NOT NULL,
"img" BYTEA,
PRIMARY KEY("name")
) WITH OIDS;

III - Interaction avec la base de données

Nous avons donc la possibilité d'utiliser une base de données MySQL ou PostgreSQL. Je vous propose pour cela d'utiliser un fichier de configuration pour gérer les paramètres de connexion propres à chaque base de données. Nous pourrions bien entendu ajouter des informations sur le nom de la table ainsi que les noms des champs, mais le but principal est d'y mettre à la fois des données comme « url » et « driver » propres à chaque SGBDR, ainsi que les données utilisateur/mot de passe sur lesquelles le lecteur ne pourra peut-être pas agir s'il cherche à mettre en pratique le contenu de ce tutoriel.

Appelons ces fichiers respectivement proprieteMysqlBLOB.dat et proprietePostgresqlBLOB.dat :

proprieteMysqlBLOB.dat

proprietePostgresqlBLOB.dat

user=toto
password=titi
url=jdbc\:mysql\://localhost/blob
driver=com.mysql.jdbc.Driver

user=toto
password=titi
url= jdbc\:postgresql\://localhost\:5432/blob
driver=org.postgresql.jdbc.Driver

On notera l'utilisation d'anti slash \ pour échapper les doubles points dans les chaînes.

Il convient bien entendu d'adapter les paramètres en fonction de votre configuration. Notamment le nom de la base de données (blob) pour MySQL ou Postgresql.

III-A - Connexion/Déconnexion

Nous pouvons dès lors utiliser ces fichiers de configuration pour définir une méthode d'initialisation de la connexion.

Nous allons donc entamer la partie JAVA de ce tutoriel, en créant un package images et une première classe Bdd.

Pour récupérer les informations présentes dans un fichier de configuration, nous aurons besoin d'utiliser java.util.Properties.

Voici donc le contenu de la classe Bdd pour permettre d'établir une connexion :

package images;

import java.sql.*;
import java.util.*;
import java.io.*;

public class Bdd
{

private Connection conn;

public Bdd()
{
}

public void initialiserConnexion(String fichierConfig) throws Exception
{
Properties propBD = new Properties();

try
{
FileInputStream entree = new FileInputStream(fichierConfig);
propBD.load(entree);
}
finally
{
entree.close();
}

Class.forName(propBD.getProperty("driver"));
conn = DriverManager.getConnection(propBD.getProperty("url"),
propBD.getProperty("user"),propBD.getProperty("password"));
}

public void deconnexion() throws Exception
{
if(conn != null)
{
conn.close();
}
}

}

III-B - Enregistrement d'une image

Pour enregistrer une image, nous allons suivre le processus suivant : on insère une nouvelle ligne avec pour contenu le nom qu'on souhaite donner à l'image, puis on fait une mise à jour pour insérer l'image.

Pour cela, nous allons nous reposer sur le paquetage java.sql et notamment la classe PreparedStatement (pour une requête paramétrée).

public void sauveIMG(String location, String name) throws Exception
{
File monImage = new File(location);
FileInputStream istreamImage = new FileInputStream(monImage);
try
{
PreparedStatement ps = conn.prepareStatement("insert into image (name, img) values (?,?)");
try
{
ps.setString(1, name);
ps.setBinaryStream(2, istreamImage, (int) monImage.length());
ps.executeUpdate();
}
finally
{
ps.close();
}
}
finally
{
istreamImage.close();
}
}

Les paramètres dans un PreparedStatement se matérialisent par un point d'interrogation. L'affectation du contenu du paramètre se fait par la méthode setXXX (ici par exemple setString) dont le premier argument est la position du paramètre dans la requête.
Les requêtes de sélection s'exécutent en général avec la méthode executeQuery(), les requêtes de mise à jour s'exécutent elles à l'aide de executeUpdate(). Il est néanmoins possible, lorsqu'on cherche à exécuter une requête qu'on ne connaît pas, d'utiliser la méthode execute().

Concernant la partie spécifique à la copie des informations extraites du fichier à la base de données, on s'appuie sur des composants gérant un flux binaire. On commence donc par définir le fichier à consulter et on lui associe un flux entrant. Le flux est ensuite affecté à la colonne img de la table à l'aide de la méthode setBinaryStream dont les second et troisième paramètres correspondent respectivement au flux entrant et à la taille à extraire.

La méthode setBinaryStream prend pour troisième paramètre un entier non signé int, ce qui veut dire que la valeur maximale sera de 2 147 483 647, soit l'assurance de pouvoir gérer un fichier allant jusqu'à plus de 2 Go.

III-C - Chargement d'une image

Nous allons également réaliser l'opération inverse, c'est-à-dire enregistrer sur notre disque dur une image contenue dans la base de données.

public void chargeIMG(String name, String location) throws Exception
{
File monImage = new File(location);
FileOutputStream ostreamImage = new FileOutputStream(monImage);

try
{
PreparedStatement ps = conn.prepareStatement("select img from image where name=?");

try
{
ps.setString(1,name);
ResultSet rs = ps.executeQuery();

try
{
if(rs.next())
{
InputStream istreamImage = rs.getBinaryStream("img");

byte[] buffer = new byte[1024];
int length = 0;

while((length = istreamImage.read(buffer)) != -1)
{
ostreamImage.write(buffer, 0, length);
}
}
}
finally
{
rs.close();
}
}
finally
{
ps.close();
}
}
finally
{
ostreamImage.close();
}
}

Le principe du transfert des données est identique à ce qui est utilisé dans la méthode de sauvegarde. On notera l'utilisation d'une variante dans la méthode getBinaryStream() pour laquelle on peut également fournir comme paramètre le nom de la colonne cible.
Le transfert se fait quant à lui à l'aide d'un buffer intermédiaire et permet de lire morceau par morceau le contenu en base pour le recopier dans le flux de sortie relié à un fichier.

III-D - Liste des noms d'images

public Vector getAllNames() throws Exception
{
Vector res = new Vector();

Statement stmt = conn.createStatement();

try
{
ResultSet rset = stmt.executeQuery("select name from image");

try
{
while(rset.next())
{
res.add(rset.getString("name"));
}
}
finally
{
rset.close();
}
}
finally
{
stmt.close();
}

return res;
}

IV - Mise en place d'une application graphique

La classe que nous venons de réaliser est bien entendu opérationnelle et vous pouvez très bien là tester en écrivant une méthode main. Néanmoins, il est plus agréable de bénéficier d'une petite interface graphique permettant de gérer les différentes opérations.

Pour cela, nous allons nous appuyer sur le composant JFileChooser (voir la FAQ JAVA) pour la sélection d'un fichier, ainsi qu'une boîte de dialogue très simple pour la saisie d'un nom, le tout exploité par une JFrame avec un menu.

IV-A - JFileChooser et gestion des extensions

Pour pouvoir filtrer suivant des extensions, nous devons définir une classe particulière que nous appellerons ExtensionFileFilter héritant de javax.swing.filechooser.FileFilter.

Voici le contenu de la classe utilisée :

package images;
import java.io.File;
import javax.swing.filechooser.FileFilter;

public class ExtensionFileFilter extends FileFilter
{

String description; // description du filtre
String[] extensions; // liste des extensions

// le constructeur pour une seule extension

public ExtensionFileFilter(String description, String[] extensions)
{
super();
this.description = description;
this.extensions = (String[]) extensions;
}

// le constructeur pour une liste d'extensions

public ExtensionFileFilter(String description, String extension)
{
this(description,new String[]{extension});
}

// redéfinition de la méthode accept

public boolean accept(File file)
{
if(file.isDirectory())
{
return true;
}

String nomFichier = file.getPath();
int n = extensions.length;
for(int i=0; i<n; i++)
{
if(nomFichier.endsWith(extensions[i]))
{
return true;
}
}
return false;
}

public String getDescription()
{
return description;
}
}

Il sera ainsi possible de définir un filtre pour une boîte de dialogue de sélection de fichier.

IV-B - Boîte de dialogue simple avec JOptionPane

Il nous faut également demander à l'utilisateur de saisir un nom identifiant l'image en base de données. Pour cela nous utiliserons une boîte de dialogue assez simple dont voici un exemple.

String name;
String message = "Quel nom donner à ce fichier en base ?";
name = JOptionPane.showInputDialog(message);
if(name != null)
{
...
}

Il y a bien sûr des variantes possibles, se référer à la doc sur le site de sun pour plus d'informations.

IV-C - La classe ImageFrame

Après avoir précisé les quelques éléments spécifiques utilisés pour notre Frame, nous pouvons désormais nous occuper de la classe principale qui va correspondre à notre interface graphique.

package images;

import java.awt.*;
import java.io.*;
import java.util.*;
import javax.swing.*;
import java.awt.event.*;
import javax.swing.filechooser.FileFilter;

public class ImageFrame extends JFrame
{
// éléments du menu
private JMenuBar menuBar = new JMenuBar();
private JMenu menuFichier = new JMenu();
private JMenu menuCharger = new JMenu();
private JMenuItem menuFichierSelection = new JMenuItem();
private JMenuItem menuFichierQuitter = new JMenuItem();
private JMenuItem menuFichierEnregistrer = new JMenuItem();

private BorderLayout layoutMain = new BorderLayout();

// éléments de sélection de fichiers
private FileFilter fileFilter = null;
private FileFilter datFilter = null;
private JFileChooser fileChooser = new JFileChooser("."); // démarrage dans le répertoire courant
private JFileChooser bddChooser = new JFileChooser(".");

// membre permettant de gérer l'interaction avec la base de données
private Bdd gestionBdd = null;

public ImageFrame()
{
try
{
jbInit();
}
catch(Exception e)
{
e.printStackTrace();
}
}

private void jbInit() throws Exception
{
this.setJMenuBar(menuBar);
this.getContentPane().setLayout(layoutMain);
this.setTitle("Gestionnaire d'images");
this.setBackground(SystemColor.control);

// gestion de la fermeture
this.addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
quitter();
}
});

// initialisation des filtres pour les différentes sélections
String[] filters = new String[]{"jpg","bmp","gif"};
fileFilter = new ExtensionFileFilter("images",filters);
fileChooser.addChoosableFileFilter(fileFilter);

datFilter = new ExtensionFileFilter("propriété bdd","dat");
bddChooser.addChoosableFileFilter(datFilter);

// gestion des différents éléments du menu

menuFichier.setText("Fichier");
menuFichierSelection.setText("Selectionner la base");
menuFichierEnregistrer.setText("Enregistrer dans la bdd");
menuFichierEnregistrer.setEnabled(false);
menuCharger.setText("Charger");
menuFichierQuitter.setText("Quitter");

menuFichierSelection.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
int status = bddChooser.showDialog(null,"Sélection du fichier de configuration de la base");

if(status == JFileChooser.APPROVE_OPTION)
{
File file = bddChooser.getSelectedFile();
menuFichierEnregistrer.setEnabled(false);
try
{
gestionBdd.deconnexion();
gestionBdd.initialiserConnexion(file.getAbsolutePath());
initMenuCharger(); // on récupère les noms des images présentes en base
menuFichierEnregistrer.setEnabled(true);
}
catch(Exception ex)
{
JOptionPane.showMessageDialog(null,"Une erreur s'est produite dans l'initialisation de la connexion");
menuCharger.removeAll(); // on supprime les éléments du menu Charger
ex.printStackTrace();
}
}
}
}
);

menuFichierEnregistrer.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
int status = fileChooser.showDialog(null,"Sélection du fichier à sauver");

if(status == JFileChooser.APPROVE_OPTION)
{
File file = fileChooser.getSelectedFile();
String name;
String message = "Quel nom donner à ce fichier en base ?";
name = JOptionPane.showInputDialog(message);
if(name != null)
{
try
{
gestionBdd.sauveIMG(file.getAbsolutePath(),name);
initMenuCharger(); // on recharge la liste des images
}
catch(Exception ex)
{
JOptionPane.showMessageDialog(null,"Une erreur s'est produite dans l'enregistrement de l'image.");
ex.printStackTrace();
}
}
}
}
});

menuFichierQuitter.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent ae)
{
quitter();
}
});

menuFichier.add(menuFichierSelection);
menuFichier.add(menuFichierEnregistrer);
menuFichier.add(menuFichierQuitter);
menuBar.add(menuFichier);
menuBar.add(menuCharger);

}

private void initMenuCharger()
{
try
{
Vector v = new Vector(gestionBdd.getAllNames());
Iterator it = v.iterator();
menuCharger.removeAll(); // on supprime les éléments du menu Charger
while(it.hasNext())
{
/* création dynamique des éléments du menu charger
avec création d'un listener. */
final String name = (String) it.next();
JMenuItem item = new JMenuItem(name);
item.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
int status = fileChooser.showDialog(null,"Saisir l'emplacement et le nom du fichier cible");

if(status == JFileChooser.APPROVE_OPTION)
{
try
{
gestionBdd.chargeIMG(name,fileChooser.getSelectedFile().getAbsolutePath());
}
catch(Exception ex)
{
JOptionPane.showMessageDialog(null,"Une erreur s'est produite dans le chargement de l'image.");
ex.printStackTrace();
}
}
}
}
);

menuCharger.add(item); // on ajoute l'élément dans le menu
}
}
catch(Exception ex)
{
JOptionPane.showMessageDialog(null,"Une erreur s'est produite lors de la récupération des noms d'image.");
ex.printStackTrace();
}
}

// permet de définir le gestionnaire de base de données
public void setGestionnaireBdd(Bdd bdd)
{
this.gestionBdd = bdd;
}

public void quitter()
{
try
{
gestionBdd.deconnexion();
}
catch(Exception ex)
{
JOptionPane.showMessageDialog(null,"Une erreur s'est produite lors de la déconnexion.");
ex.printStackTrace();
}
System.exit(0);
}

public static void main(String[] args)
{
ImageFrame f = new ImageFrame();
f.setSize(300,300);
Bdd gestionnaireBdd = new Bdd();
f.setGestionnaireBdd(gestionnaireBdd);
f.show();
}
}

V - Utilisation d'un jar

Notre application est maintenant utilisable. Il ne reste plus qu'à se préoccuper du déploiement afin que n'importe qui puisse l'utiliser sans avoir à se préoccuper des librairies nécessaires.

Pour cela, nous allons réaliser un jar, plus agréable à exécuter, et évitant à l'utilisateur de devoir traîner les différentes classes que nous avons définies.
Après avoir compilé et créé vos .class dans le répertoire classes/images/, vous êtes prêt à préparer votre jar.

V-A - Rassembler les librairies

Nous avons choisi de faciliter l'utilisation de notre petite application en fournissant les librairies nécessaires, ce qui veut dire qu'une fois notre jar créé, il ira rechercher les classes nécessaires dans une liste de librairies que nous lui aurons donnée.

Nous allons donc créer (dans classes/ par exemple) un répertoire lib qui va contenir nos différents jar, à savoir pour ce qui a été utilisé :

- javax-ssl-1_1.jar

- javax-ssl-1_2.jar

- mysql-connector-java-3.0.10-stable-bin.jar

- postgre74.213.jdbc3.jar

Les deux premiers jar m'ont été nécessaires pour la base de données PostgreSQL.
Libre à vous de rajouter d'autres jar si vous avez d'autres besoins.

V-B - Création d'un manifest

Lorsque vous créez un jar, sa structure est la suivante :

- Fichiers

- Répertoire META-INF

Le répertoire META-INF contient entre autres un fichier nommé MANIFEST.MF dans lequel sont notamment recensées des informations sur le package et sur les librairies utilisées.

Le but est donc de définir un tel fichier permettant de fournir les informations nécessaires au bon fonctionnement de notre application.

Compte tenu des jar qui risquent d'être nécessaires à l'exécution, il faudra recenser ceux-ci, mais il faudra également définir quelle est la classe à exécuter (c'est-à-dire celle dont on va exécuter la méthode main lors de l'exécution du jar).

Pour ce faire, nous allons créer un fichier « manifest.txt » (toujours dans classes/) où nous allons mettre les informations suivantes :

Main-Class: images/ImageFrame
Class-Path: lib/postgre74.213.jdbc3.jar lib/javax-ssl-1_2.jar lib/javax-ssl-1_1.jar
lib/mysql-connector-java-3.0.10-stable-bin.jar

Les informations pour Class-Path sont sur une même ligne et chaque jar est séparé du précédent par un espace.

V-C - Création du jar

Tout est maintenant en place pour créer le jar.
Plaçons-nous en ligne de commande dans le répertoire classes et tapons la commande suivante :

jar cvfm images.jar manifest.txt images

Nous venons de créer notre jar sous le nom de images.jar.

V-D - Exécution du jar

La principale obligation pour pouvoir lancer notre jar et travailler sur les 2 bases de données mentionnées dans ce tutoriel est d'avoir dans le répertoire où images.jar sera placé également présent le répertoire lib créé précédemment. En effet, les informations présentes dans le manifest indiquent où chercher les librairies, et nous avons indiqué des adresses relatives au jar.

Il suffit ensuite de lancer notre petite application par :

java -jar images.jar

VI - Quelques liens utiles

Vous pouvez bien entendu télécharger les sources et l'application sous forme de jar.

- Les images en base de données par SQLPro

- Les LOBS (ou Larges Objects) avec ORACLE par Helyos

- Installer et configurer MySQL sous Linux par Olivier Nepomiachty

- Installer et configurer PostgreSQL sous Linux par Stessy

- La FAQ JDBC