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▲
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 |
user=toto |
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