Accès à une base de données avec JDBC
Accès à une base de données avec JDBC
La deuxième solution est bien sûr préférable et c’est celle-ci que les concepteurs de Java ont choisie. Ils
ont donc développé l’API JDBC (Java DataBase Connectivity) pour l’accès à une base de données. Plus
précisément l’API JDBC est composée de deux parties. La première partie contenue dans le package
[Link] est essentiellement composée d’interfaces. Ces interfaces sont implémentées par le pilote JDBC.
Ce pilote n’est pas développé par Oracle mais en général par le concepteur de la base de données. C’est
effectivement ce dernier qui maîtrise le mieux la technique pour communiquer avec la base de données. Il
existe quatre types de pilotes JDBC avec des caractéristiques et des performances différentes.
Ce type de pilote n’est pas spécifique à une base de données mais il traduit simplement les
instructions jdbc en instructions ODBC. C’est ensuite le pilote odbc qui assure la
communication avec la base de données. Cette solution
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -1-
Samuel KAMDOUM FOLONG
n’offre que des performances médiocres. Ceci est principalement lié au nombre de couches
logicielles mises en œuvre. Les fonctionnalités jdbc sont également limitées par les
fonctionnalités de la couche ODBC. Ce type de pilote n’est plus disponible à partir de la version
8 de Java. Oracle recommande l’utilisation du pilote spécifique à la base de données que vous
souhaitez utiliser. Il est généralement disponible sur le site du concepteur de la base de
données.
Ce type de pilote n’est pas entièrement écrit en Java. La partie de ce pilote écrite en Java
effectue simplement des appels vers des fonctions du pilote natif. Ces appels sont effectués
grâce à l’API JNI (Java Native Interface). Comme pour les pilotes de type 1, il y a donc une
traduction nécessaire entre le code Java et la base de données. Cependant, ce type de pilote
est tout de même plus efficace que les pilotes JDBC-ODBC.
Ce type de pilote représente la solution idéale puisqu’il n’y a aucun intermédiaire. Le pilote
transmet directement les requêtes à la base de données en utilisant le protocole propre à la
base de données. La majorité des pilotes sont maintenant de ce type.
1. Présentation de JDBC
L’API JDBC fournit un ensemble de classes et surtout d’interfaces permettant la manipulation d’une base
de données. Ces éléments représentent tout ce dont vous avez besoin pour accéder aux données à partir
d’une application Java.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -2-
Samuel KAMDOUM FOLONG
La classe DriverManager est notre point de départ. C’est elle qui assure la liaison avec le pilote. C’est
par son intermédiaire que nous pouvons obtenir une connexion vers la base de données. Celle-ci est
représentée par une instance de classe implémentant l’interface Connection . Cette connexion est
ensuite utilisée pour transmettre des instructions vers la base. Les requêtes simples sont exécutées grâce
à l’interface Statement , les requêtes avec paramètres le sont avec l’interface PreparedStatement
et les procédures stockées avec l’interface CallableStatement .
Les éventuels enregistrements sélectionnés par l’instruction SQL sont accessibles avec un élément
Resultset . Nous allons détailler ces différentes étapes dans les sections suivantes.
2. Chargement du pilote
La première étape indispensable est d’obtenir le pilote JDBC adapté à votre base de
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -3-
Samuel KAMDOUM FOLONG
données. En général, ce pilote est disponible en téléchargement sur le site du concepteur de la base de
données. Pour nos exemples, nous utiliserons le pilote fourni par Oracle pour accéder à un serveur de
base de données MySQL. La version 8.0 du pilote (nommé Connector/J) est nécessaire pour utiliser
MySQL 8.0. Il peut être téléchargé à l’adresse suivante : [Link]
Il est aussi possible de télécharger plus simplement ce pilote sur le dépôt Maven à l’adresse suivante :
[Link]
L’écran suivant est affiché. Il permet le téléchargement direct du pilote en cliquant sur jar (2.4MB) ou en
utilisant un gestionnaire de dépendances comme Maven ou Gradle.
Le fichier archive ainsi obtenu ([Link]) contient les classes développées par
Oracle qui implémentent les différentes interfaces JDBC. Ce fichier devra bien sûr être accessible au
moment de la compilation et de l’exécution de l’application.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -4-
Samuel KAMDOUM FOLONG
Copier le fichier [Link] dans un répertoire nommé lib dans votre projet.
Faites un clic droit sur votre projet et sélectionnez le menu Build Path - Configure Build Path….
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -5-
Samuel KAMDOUM FOLONG
Le répertoire lib contient physiquement le pilote et celui-ci est référencé dans le projet (Referenced
Libraries). Le pilote est utilisable pour le développement et sera disponible dans le classpath à l’exécution.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -6-
Samuel KAMDOUM FOLONG
Le pilote est chargé automatiquement à l’exécution du projet depuis la version 4.0 de l’API JDBC (mise à
disposition depuis Java SE 6).
Auparavant, le pilote devait être chargé grâce à la méthode forName de la classe Class . Cette méthode
attend comme paramètre une chaîne de caractères contenant le nom du pilote. Il est à ce niveau
indispensable de parcourir la documentation du pilote pour obtenir le nom de la classe. Dans notre cas,
cette classe porte le nom suivant :
[Link]
[Link]("[Link]");
Les chaînes de caractères passées comme paramètres représentent des noms de classes, elles sont
donc sensibles à la casse. L’instruction forName doit d’ailleurs être protégée par un bloc try catch
car elle est susceptible de déclencher une exception de type ClassNotFoundException .
Après son chargement, le pilote est maintenant capable de fournir une connexion vers le serveur de base
de données.
a. Établir la connexion
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -7-
Samuel KAMDOUM FOLONG
cependant standard avec la forme suivante jdbc:nomDuProtocole . Le nom du protocole est propre
à chaque pilote et c’est grâce à ce nom que la méthode getConnection est capable d’identifier le bon
pilote. Le reste de la chaîne est spécifique à chaque pilote. Il contient en général les informations
permettant d’identifier le serveur et la base de données sur ce serveur vers laquelle la connexion doit être
établie. Pour le pilote MySQL, la syntaxe de base est la suivante :
jdbc:mysql://adresseIP/nomDeLaBasDeDonnees?user=nomUtilisateur&
password=motDePasseUtilisateur;
Voici un exemple permettant d’établir une connexion vers une base de données. Cet exemple part du
principe que vous avez un serveur MySQL installé sur la machine de développement avec une base de
données nommée demo_java et un compte utilisateur mysqluser identifié par le mot de passe Pa$$w0rd.
package [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -8-
Samuel KAMDOUM FOLONG
}
}
Pour que ce code compile, il est nécessaire de charger le module [Link] car il n’est pas chargé par
défaut. Pour cela, il suffit de modifier le fichier [Link] comme ceci :
module ProjetJavaSE_Chapitre6 {
requires [Link];
}
Il se peut, en fonction de la configuration de votre machine, que vous obteniez le message d’erreur
suivant :
Cela signifie qu’il y a un problème de détermination du fuseau horaire à utiliser. Il est possible de corriger
le problème en ajoutant un paramètre à la chaîne de connexion :
jdbc:mysql://localhost/demo_java?serverTimezone=UTC
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG -9-
Samuel KAMDOUM FOLONG
cela, positionnez-vous sur l’écran Server Variables depuis MySQL Workbench en cliquant sur le menu
MANAGEMENT - Status and System Variables. Recherchez la propriété time_zone et ajustez-la avec la
valeur souhaitée (+02 :00 dans l’exemple) :
b. Manipuler la connexion
Une connexion est ouverte dès sa création. Il n’y a donc pas de méthode permettant d’ouvrir une
connexion. Cependant, une connexion peut être fermée en appelant la méthode close . Il est important
de le faire lorsque la connexion n’est plus utile pour libérer les ressources engagées. Après la fermeture
d’une connexion, il n’est plus possible de l’utiliser et elle doit être recréée pour pouvoir être à nouveau
utilisable.
L’interface Connection étend l’interface AutoCloseable . Il n’est donc pas nécessaire d’utiliser la
méthode close si le développeur fait usage de l’instruction try with resources . L’exemple suivant
montre l’utilisation d’une telle instruction :
package [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 10 -
Samuel KAMDOUM FOLONG
La fonction isClosed permet de vérifier si une connexion est fermée. Si cette fonction retourne un
boolean qui est égal à false , on peut donc légitimement penser que la connexion est ouverte et permet
donc de dialoguer avec la base de données. Ce n’est en fait pas toujours le cas. La connexion peut parfois
être dans un état intermédiaire : elle n’est pas fermée mais elle ne peut pas être utilisée pour transférer
des instructions vers le serveur. Pour vérifier la disponibilité de la connexion, vous pouvez utiliser la
méthode isValid . Cette méthode teste réellement la disponibilité de la connexion en essayant
d’envoyer une instruction SQL et en vérifiant qu’elle obtient bien une réponse de la part du serveur. Cette
méthode n’est pas implémentée dans tous les pilotes et si elle ne l’est pas, son appel déclenche une
exception du type [Link] ou une erreur du
type [Link] . Voici le code mettant en évidence l’utilisation de cette
méthode :
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 11 -
Samuel KAMDOUM FOLONG
boolean estValide;
try {
estValide = [Link](0 );
if (estValide) {
[Link]("La connexion est valide");
} else {
[Link]("La connexion n'est pas valide");
}
} catch ( Exception e) {
[Link]("La méthode isValid n'est pas prise en
charge par le pilote");
[Link]();
}
}
Si vous effectuez uniquement des opérations de lecture sur la base de données vous pouvez optimiser la
communication en précisant que la connexion est en lecture seule. La méthode setReadOnly permet
de modifier ce paramétrage de la connexion. L’état peut ensuite être testé en utilisant la méthode
isReadOnly . Pour certains pilotes cette fonctionnalité n’est pas implémentée et la méthode
setReadOnly n’a aucun effet. La fonction suivante vérifie si cette fonctionnalité est disponible pour la
connexion qui lui est passée comme paramètre.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 12 -
Samuel KAMDOUM FOLONG
else
{
[Link]
.println("le mode lecture seule n'est pas
pris en charge par ce pilote");
}
[Link](etat);
}
catch (SQLException e)
{
[Link]();
}
}
Lors de l’exécution d’une instruction SQL, le serveur peut détecter des problèmes et de ce fait générer des
avertissements. Ces avertissements peuvent être récupérés par la méthode getWarnings de la
connexion. Cette méthode retourne un objet SQLWarning représentatif du problème rencontré par le
serveur. Si plusieurs problèmes sont rencontrés par le serveur celui-ci génère plusieurs objets
SQLWarning chaînés les uns aux autres. La méthode getNextWarning permet d’obtenir l’élément
suivant ou null si la liste est terminée. La liste peut être vidée avec la méthode clearWarnings . La
fonction suivante affiche tous les avertissements reçus par la connexion.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 13 -
Samuel KAMDOUM FOLONG
[Link]([Link]());
[Link]([Link]());
[Link]([Link]());
avertissement=[Link]();
}
}
[Link]();
}
catch (SQLException e)
{
[Link]();
}
}
En général, lors de la création de la chaîne de connexion, l’un des paramètres détermine le nom de la base
vers laquelle vous souhaitez établir une connexion. La modification du nom de la base de données à
laquelle vous êtes connecté se réalise avec la méthode setCatalog à laquelle il faut fournir le nom
d’une autre base présente sur le même serveur. Il faut bien sûr que le compte avec lequel vous avez ouvert
la connexion dispose des droits d’accès suffisants pour cette base de données. À noter qu’avec cette
méthode, nous changeons de base de données mais la connexion concerne toujours le même serveur. Il
n’y a aucun moyen de changer de serveur sans créer une nouvelle connexion.
Le code suivant :
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 14 -
Samuel KAMDOUM FOLONG
Il faut bien sûr, au préalable, avoir créé une nouvelle base de données nommée demo_java_2 .
La structure de la base peut également être obtenue avec la méthode getMetaData . Cette méthode
retourne un objet de type DatabaseMetaData fournissant de très nombreuses informations sur la
structure de la base. La fonction suivante affiche une analyse rapide de ces informations.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 15 -
Samuel KAMDOUM FOLONG
{
[Link]([Link](i)+"\t");
}
[Link]();
}
[Link]();
rs=[Link](null,null,"%");
[Link]("les procédures stockées");
[Link]("base\tschema\tnom procedure");
while([Link]())
{
for ( int i = 1 ; i <=3 ; i++)
{
[Link]([Link](i)+"\t");
}
[Link]();
}
[Link]();
}
catch (SQLException e)
{
[Link]();
}
}
Vous pouvez observer dans cet exemple l’utilisation d’un objet de type ResultSet . Il permet de lire les
jeux de résultats obtenus auprès du serveur. Une section dédiée est disponible un peu plus loin dans le
chapitre.
Toutes ces méthodes présentées jusque-là peuvent un jour ou l’autre rendre service mais le but principal
d’une connexion est de permettre l’exécution d’instructions SQL. Ce sont les objets de type Connection
qui vont fournir les objets nécessaires pour l’exécution de ces instructions. Trois types d’instructions SQL
peuvent être exécutés :
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 16 -
Samuel KAMDOUM FOLONG
ˇ
Statement pour les requêtes simples,
ˇ
PreparedStatement pour les requêtes précompilées,
ˇ
CallableStatement pour les procédures stockées.
Et enfin une instance de ces classes peut être obtenue par une méthode différente d’un objet de type
Connection :
ˇ
createStatement pour les objets de type Statement ,
ˇ
prepareStatement pour les objets de type PreparedStatement ,
ˇ
prepareCall pour les objets de type CallableStatement .
L’utilisation de ces méthodes et de ces objets est détaillée dans la section suivante.
Avant l’exécution d’une instruction SQL vous devez choisir le type d’objet le plus approprié. Les rubriques
suivantes décrivent les trois types d’objets disponibles et leur utilisation.
Un objet de ce type est obtenu par la méthode createStatement de la connexion. Deux versions de
cette méthode sont disponibles.
La première n’attend aucun paramètre. Dans ce cas, si l’objet Statement est utilisé pour exécuter une
instruction SQL générant un jeu d’enregistrements (select ), celui-ci sera en lecture seule et avec un
défilement en avant uniquement.
Les informations présentes dans ce jeu d’enregistrements ne pourront pas être modifiées et le parcours
du jeu d’enregistrements ne pourra se faire que du premier enregistrement vers le dernier enregistrement.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 17 -
Samuel KAMDOUM FOLONG
généré. Elle accepte deux paramètres. Le premier détermine le type du jeu d’enregistrements.
ˇ
ResultSet.TYPE_FORWARD_ONLY : le jeu d’enregistrements sera à défilement en avant
seulement.
ˇ
ResultSet.TYPE_SCROLL_INSENSITIVE : le jeu d’enregistrements pourra être
parcouru dans les deux sens mais sera insensible aux changements effectués dans la base de
données par d’autres utilisateurs.
ˇ
ResultSet.TYPE_SCROLL_SENSITIVE : le jeu d’enregistrements pourra être parcouru
dans les deux sens et sera sensible aux changements effectués dans la base de données par
d’autres utilisateurs.
Le second paramètre détermine les possibilités de modification des informations contenues dans le jeu
d’enregistrements. Les deux constantes suivantes sont définies :
ˇ
ResultSet.CONCUR_READ_ONLY : les enregistrements sont en lecture seule.
ˇ
ResultSet.CONCUR_UPDATABLE : les enregistrements peuvent être modifiés dans le jeu
d’enregistrements.
Cet objet est le plus élémentaire permettant l’exécution d’instructions SQL. Il peut prendre en charge
l’exécution de n’importe quelle instruction SQL. Vous pouvez donc exécuter aussi bien des instructions du
DDL (Data Definition Language) que des instructions du DML (Data Manipulation Language). Il faut
simplement choisir dans cet objet la méthode la plus adaptée pour l’exécution du code SQL. Le choix de
cette méthode est dicté par le type de résultat que doit fournir l’instruction SQL. Quatre méthodes sont
disponibles :
ˇ
public boolean execute(String sql) : cette méthode permet l’exécution de
n’importe quelle instruction SQL. Le boolean retourné par cette méthode indique si un jeu
d’enregistrements a été généré (true ) ou si simplement l’instruction a modifié des
enregistrements dans la base de données (false ). Si un jeu d’enregistrements est généré, il
peut être obtenu par la méthode getResultSet . Si des enregistrements ont été modifiés
dans la
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 18 -
Samuel KAMDOUM FOLONG
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
BufferedReader br =
new BufferedReader(new InputStreamReader([Link]));
[Link]("saisir votre
instruction SQL :");
String requete = [Link]();
boolean resultat = [Link](requete);
//Si vrai, il y a eu un résultat
if (resultat) {
[Link]("votre instruction a généré
un jeu d\'enregistrements");
//Récupération du jeu de résultats
ResultSet rs = [Link]();
//Positionnement sur le dernier
enregistrement
[Link]();
//Lecture de numéro de la ligne
[Link]("il contient " +
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 19 -
Samuel KAMDOUM FOLONG
L’exécution de ce code avec une requête comportant des erreurs provoque le résultat suivant :
L’exécution de ce code avec une requête de type select provoque le résultat suivant :
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 20 -
Samuel KAMDOUM FOLONG
Pour l’exemple, une table PERSONNES a été créée avec le script suivant :
use demo_java;
create table PERSONNES(
id int auto_increment primary key,
nom varchar( 30) not null,
prenom varchar( 30) not null,
dateDeNaissance date
);
Pour terminer, l’exécution de ce code avec une requête d’insertion provoque le résultat suivant :
ˇ
public Resultset executeQuery(String requete) : cette méthode est conçue
spécialement pour l’exécution d’instructions select . Le jeu d’enregistrements est disponible
directement comme valeur retournée par la fonction.
ˇ
public int executeUpdate(String requŒte) : cette méthode est parfaitement
adaptée pour l’exécution d’instructions modifiant le contenu de la base de données comme les
instructions insert , update , delete . L’entier
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 21 -
Samuel KAMDOUM FOLONG
retourné par cette fonction indique le nombre d’enregistrements affectés par la modification.
ˇ
public int[] executeBatch() : cette méthode permet d’exécuter un ensemble
d’instructions SQL par lot. Le lot d’instructions à exécuter doit être préparé au préalable avec
les méthodes addBatch et clearBatch . La première reçoit comme paramètre une chaîne
de caractères représentant une instruction SQL à ajouter au lot. La seconde permet de
réinitialiser le lot d’instructions. Il n’est pas possible de supprimer une instruction particulière
du lot. Vous ne devez pas ajouter au lot d’instruction SQL générant un jeu de résultats, car dans
ce cas la méthode updateBatch déclenche une exception de type
BatchUpdateException . Cette fonction retourne un tableau d’entiers permettant d’obtenir
une information sur l’exécution de chacune des requêtes du lot. Chaque case du tableau
contient un entier représentatif du résultat de l’exécution de la requête correspondante dans le
lot. Une valeur supérieure ou égale à 0 indique un fonctionnement correct de l’instruction et
représente le nombre d’enregistrements modifiés. Une valeur égale à la constante
Statement.EXECUTE_FAILED indique que l’exécution de l’instruction a échoué. Dans ce
cas, certains pilotes arrêtent l’exécution du lot alors que d’autres continuent avec l’instruction
suivante du lot. Une valeur égale à la constante Statement.SUCCESS_NO_INFO indique
que l’instruction a été exécutée correctement mais que le nombre d’enregistrements modifiés
ne peut pas être déterminé. Cette méthode est très pratique pour exécuter des modifications
sur plusieurs tables liées. Ce pourrait par exemple être le cas dans une application de gestion
de commerciale avec une table pour les commandes et une table pour les lignes de
commande. La suppression d’une commande doit dans ce cas entraîner la suppression de
toutes les lignes correspondantes. La fonction suivante vous permet de saisir plusieurs
instructions SQL et de les exécuter par lot.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 22 -
Samuel KAMDOUM FOLONG
import [Link];
import [Link];
import [Link];
import [Link];
BufferedReader br =
new BufferedReader(new InputStreamReader([Link]));
[Link]("saisir vos instructions SQL
puis run pour exécuter le lot :");
String requete = [Link]();
while () {
[Link](requete);
requete = [Link]();
}
[Link]("exécution
du lot d'instructions");
int[] resultats = [Link]();
for (int i = 0 ; i < [Link]; i++) {
switch (resultats[i]) {
case Statement.EXECUTE_FAILED:
[Link]("l'exécution
de l'instruction " + i + " a échoué");
break;
case Statement.SUCCESS_NO_INFO:
[Link]("l'exécution
de l'instruction " + i + " a réussi");
[Link]("le nombre
d'enregistrements modifiés est inconnu");
break;
default:
[Link]("l'exécution
de l'instruction " + i + " a réussi");
[Link]("elle a
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 23 -
Samuel KAMDOUM FOLONG
} catch (SQLException e) {
[Link]("votre batch n'a pas
fonctionné correctement");
} catch (IOException e) {
[Link]();
}
}
L’exécution de code avec des instructions SQL valides donne le résultat suivant :
ˇ
public void setQueryTimeOut(int duree) : cette méthode indique la durée
maximale allouée pour l’exécution d’une instruction SQL avant le déclenchement d’une
exception.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 24 -
Samuel KAMDOUM FOLONG
ˇ
public void close() : lorsqu’un objet Statement n’est plus utile dans une
application, il est préférable de le fermer explicitement en appelant cette méthode. Celle-ci
provoque la libération de toutes les ressources utilisées par cet objet. La fermeture d’un objet
Statement provoque également la fermeture du jeu d’enregistrements associé
(ResultSet ). Tout comme l’interface Connection , l’interface Statement hérite de
l’interface AutoCloseable . Il est donc possible (comme le montrent les exemples)
d’initialiser ce type d’objet dans une instruction try with resources pour provoquer sa fermeture
automatique à la fin du bloc try .
ˇ
public void setMaxRows(int nombre) : cette méthode limite le nombre de lignes
des jeux d’enregistrements générés par cet objet Statement . Si une instruction SQL génère
un jeu d’enregistrements comportant plus de lignes, les lignes excédentaires sont tout
simplement ignorées (sans plus d’information).
ˇ
public void setMaxFieldSize(int taille) : cette méthode limite la taille de
certains types de champs dans le jeu d’enregistrements. Les types de champs concernés sont
les champs d’une base de données pouvant avoir une taille variable comme par exemple les
champs caractères ou binaires. Les données excédentaires sont simplement ignorées. Les
champs concernés peuvent de ce fait être inutilisables dans l’application.
ˇ
public void setFetchSize(int nbLignes) : lorsqu’une instruction SQL génère un
jeu d’enregistrements, les données correspondantes sont transférées du serveur de base de
données vers la mémoire de l’application Java. Ce transfert est effectué par blocs en fonction
de l’utilisation des données. Cette méthode indique au pilote le nombre de lignes de chaque
bloc transféré de la base de données vers l’application.
ˇ
public boolean getMoreResults() : si la méthode execute est utilisée pour
exécuter plusieurs instructions SQL, par exemple deux instructions select , il y a dans ce cas
génération de deux jeux d’enregistrements. Le premier est obtenu par la méthode
getResultSet . Le second ne sera accessible qu’après avoir appelé la méthode
getMoreResults . Cette fonction permet de déplacer le pointeur sur le résultat suivant et
retourne un boolean égal à true si le résultat suivant est un jeu d’enregistrements. Si c’est
le cas, il
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 25 -
Samuel KAMDOUM FOLONG
La fonction suivante permet l’exécution de plusieurs instructions SQL séparées par des points-virgules :
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
BufferedReader br =
new BufferedReader(new InputStreamReader([Link]));
[Link]("saisir vos instructions SQL
séparées par ; :");
String requete = [Link]();
resultat = [Link](requete);
int i = 1 ;
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 26 -
Samuel KAMDOUM FOLONG
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 27 -
Samuel KAMDOUM FOLONG
} catch (IOException e) {
[Link]();
}
}
Pour que l’exécution de requêtes multiples fonctionne avec MySQL, il est nécessaire d’ajouter le paramètre
allowMultiQueries avec la valeur true .
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 28 -
Samuel KAMDOUM FOLONG
Il arrive fréquemment d’avoir à faire exécuter plusieurs fois une requête SQL avec juste une petite
modification entre deux exécutions. L’exemple classique correspond à une requête de sélection avec une
restriction.
La valeur sur laquelle porte la restriction est en général saisie par l’utilisateur de l’application et dans ce
cas disponible dans une variable. La première solution qui vient à l’esprit consiste à construire la requête
SQL par concaténation de plusieurs chaînes de caractères.
String requete = "select * from PERSONNES where nom = '" + nomSaisie + "'";
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 29 -
Samuel KAMDOUM FOLONG
Si l’utilisateur saisit les valeurs « admin » et « mdp1234 », alors la requête générée est la suivante :
En revanche, si l’utilisateur saisit les valeurs « admin » et « ’ or ’a’=’a », alors la requête générée est la
suivante :
Select * from PERSONNES where login = 'admin' and motDePasse = '' or 'a'= 'a' ;
La requête prend un autre aspect et le comportement est changé. Un pirate peut ainsi se connecter
sans connaître le mot de passe de l’utilisateur. C’est ce que l’on appelle une injection SQL. La faille
dans cet exemple est d’interpréter la quote saisie par l’utilisateur comme un élément de la syntaxe
SQL. Il faut échapper le caractère pour indiquer qu’il doit être interprété comme un caractère textuel
uniquement.
Les concepteurs de JDBC ont prévu une solution efficace pour pallier ces inconvénients. L’objet
PreparedStatement apporte une solution efficace à ce problème en permettant la création de
requêtes avec paramètres. Dans ce type de requête, les paramètres sont remplacés par des points
d’interrogation. Avant l’exécution de la requête, il faut fournir à l’objet PreparedStatement les valeurs
qu’il doit utiliser pour remplacer les différents points d’interrogation. Un objet PreparedStatement
peut être créé en utilisant le même principe que pour la création d’un objet Statement .
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 30 -
Samuel KAMDOUM FOLONG
Les mêmes constantes que pour la méthode createStatement peuvent être utilisées.
Avant l’exécution de l’instruction SQL, vous devez fournir une valeur pour chacun des points
d’interrogation représentant un paramètre. Pour cela, l’objet PreparedStatement dispose de
nombreuses méthodes permettant l’affectation d’une valeur à un paramètre. Chacune de ces méthodes
correspond au type de données SQL à insérer à la place d’un point d’interrogation. Ces méthodes sont
nommées selon le même schéma : setXxx où Xxx représente un type de données SQL. Chacune de ces
méthodes attend comme premier argument un entier correspondant au rang du paramètre dans
l’instruction SQL. Le premier paramètre est situé au rang 1. Le deuxième argument correspond à la valeur
à transférer dans le paramètre. Le type de cet argument correspond bien sûr au type de données SQL à
transférer vers le paramètre. Le pilote JDBC convertit ensuite le type Java en type SQL.
Par exemple la méthode setInt (int indiceParam, int value) effectue une conversion du
type int Java en type INTEGER SQL. Chaque fournisseur d’un pilote JDBC met à disposition une
documentation listant les relations existantes entre les types Java et les types SQL. Cette documentation
pour le pilote JDBC est disponible à l’adresse suivante : [Link]
j/8.0/en/[Link]
Les valeurs stockées dans les paramètres sont conservées d’une exécution à l’autre de l’instruction SQL.
La méthode clearParameters permet de réinitialiser l’ensemble des paramètres.
Les paramètres peuvent être utilisés dans une instruction SQL en remplacement de valeurs mais jamais
pour remplacer un nom de champ, encore moins un opérateur. La syntaxe suivante est bien sûr interdite :
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 31 -
Samuel KAMDOUM FOLONG
Les autres méthodes disponibles avec un objet PreparedStatement sont parfaitement identiques à
celles définies pour un objet Statement puisque l’interface PreparedStatement hérite directement
de l’interface Statement . L’exemple suivant permet d’afficher la liste des personnes répondant au nom
fourni par l’utilisateur de l’application.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
BufferedReader br =
new BufferedReader(new InputStreamReader([Link]));
[Link]("saisir le nom
de la personne recherchée :");
String nom = [Link]();
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 32 -
Samuel KAMDOUM FOLONG
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 33 -
Samuel KAMDOUM FOLONG
Le type PreparedStatement est aussi largement utilisé pour les opérations d’écriture (insert ,
update , delete ). Dans le cadre de l’insertion, une fonctionnalité très appréciable permet de récupérer
l’éventuelle clé auto-générée par la base de données (c’est typiquement le cas pour une clé primaire de
type auto-incrément).
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 34 -
Samuel KAMDOUM FOLONG
BufferedReader br =
new BufferedReader(new InputStreamReader([Link]));
[Link]("saisir le nom de la personne
à ajouter :");
String nom = [Link]();
[Link]("saisir le prénom de la personne
à ajouter :");
String prenom = [Link]();
//Appliquer les paramètres
[Link](1 , nom);
[Link](2 , prenom);
//Exécuter la requête et obtenir le résultat
[Link]();
//Récupérer le ResultSet contenant
éventuellement une clé
ResultSet rsCleGeneree = [Link]();
//S'il y a un enregistrement, c'est qu'il y a une clé
if([Link]())
{
[Link]("La clé
de l'enregistrement générée est " + [Link](1 ));
}
} catch (SQLException e) {
[Link]();
} catch (IOException e) {
[Link]();
}
}
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 35 -
Samuel KAMDOUM FOLONG
Une procédure stockée représente du code SQL stocké sur le serveur. Cette approche procure plusieurs
avantages :
En revanche, les applications deviennent beaucoup plus dépendantes du serveur de base de données. Le
changement de serveur vous obligera certainement à réécrire vos procédures stockées car la syntaxe
d’une procédure stockée est propre à chaque serveur. Les procédures stockées sont accessibles à partir
de Java grâce à l’objet CallableStatement . Comme pour les objets Statement et
PreparedStatement , c’est encore une fois la connexion qui va fournir une instance de cette classe.
C’est dans ce cas la méthode prepareCall qui doit être utilisée. Cette méthode attend comme
argument une chaîne de caractères identifiant la procédure stockée à appeler. Par contre la syntaxe de
cette chaîne de caractères est un petit peu spéciale puisqu’il ne suffit pas d’indiquer le nom de la
procédure stockée :
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 36 -
Samuel KAMDOUM FOLONG
Dans cette syntaxe, les points d’interrogation représentent les paramètres attendus par la procédure
stockée. Comme pour l’objet PreparedStatement les valeurs de ces paramètres doivent être fournies
par les méthodes setXxx correspondantes au type du paramètre.
Pour les paramètres utilisés en sortie de la procédure stockée (paramètres tagués avec le mot-clé out ),
c’est l’exécution de la procédure stockée qui va y stocker une valeur, vous devez en informer l’objet
CallableStatement en appelant la méthode registerOutParameter . Cette méthode attend
comme premier argument, l’indice du paramètre de retour de la procédure stockée, puis comme deuxième
argument le type SQL du paramètre. Ce type peut être indiqué avec une des constantes définies dans
l’interface [Link] . Après exécution de la procédure stockée, la valeur des
paramètres utilisés en sortie est accessible par les méthodes getXxx où Xxx représente le type SQL du
paramètre. Ces méthodes attendent comme argument l’indice du paramètre dans l’appel de la procédure
stockée.
Pour illustrer l’utilisation de l’objet CallableStatement , nous allons utiliser la procédure stockée
suivante :
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 37 -
Samuel KAMDOUM FOLONG
nombre de personnes pour chaque nom. Ce jeu d’enregistrements est trié selon le premier paramètre de la
procédure. Le second paramètre est de type out . Il permet de retourner à l’appelant le nombre total de
personnes.
Le code suivant permet d’exécuter cette procédure stockée et d’exploiter les résultats :
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 38 -
Samuel KAMDOUM FOLONG
if (existeResultat) {
ResultSet rs = [Link]();
while ([Link]()) {
[Link]([Link]("nom")
+ ": " + [Link]("nombrePersonnes") + " personnes");
}
}
} catch (SQLException e) {
[Link]();
} catch (IOException e) {
[Link]();
}
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 39 -
Samuel KAMDOUM FOLONG
Lorsqu’une instruction SQL select est exécutée par la méthode executeQuery d’un objet
Statement , PreparedStatement ou CallableStatement , celle-ci retourne un objet
ResultSet . C’est par l’intermédiaire de cet objet ResultSet que nous allons pouvoir intervenir sur le
jeu d’enregistrements. Nos possibilités d’action sur ce jeu d’enregistrements sont déterminées par les
caractéristiques de l’objet ResultSet . Ces caractéristiques sont fixées au moment de la création des
objets Statement , PreparedStatement ou CallableStatement en fonction des arguments
passés lors de l’appel des méthodes createStatement , prepareStatement ou prepareCall .
Le premier argument détermine le type du jeu d’enregistrements. Les constantes suivantes sont définies :
ˇ
ResultSet.TYPE_FORWARD_ONLY : le jeu d’enregistrements sera à défilement en avant
seulement.
ˇ
ResultSet.TYPE_SCROLL_INSENSITIVE : le jeu d’enregistrements pourra être
parcouru dans les deux sens mais sera insensible aux changements effectués dans la base de
données par d’autres utilisateurs.
ˇ
ResultSet.TYPE_SCROLL_SENSITIVE : le jeu d’enregistrements pourra être parcouru
dans les deux sens et sera sensible aux changements effectués dans la base de données par
d’autres utilisateurs.
Le second argument détermine les possibilités de modification des informations contenues dans le jeu
d’enregistrements. Les deux constantes suivantes sont définies :
ˇ
ResultSet.CONCUR_READ_ONLY : les enregistrements sont en lecture seule.
ˇ
ResultSet.CONCUR_UPDATABLE : les enregistrements peuvent être modifiés dans le jeu
d’enregistrements.
Il faut bien sûr que les actions exécutées sur l’objet Resultset soit compatibles avec
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 40 -
Samuel KAMDOUM FOLONG
ces caractéristiques sinon une exception sera déclenchée. Il est possible de vérifier les caractéristiques
d’un objet Resultset en utilisant les méthodes getType et getConcurrency . Le code suivant
affiche les caractéristiques du ResultSet . Il est disponible dans la classe TestTypeResultSet du
package [Link] .
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 41 -
Samuel KAMDOUM FOLONG
L’objet ResultSet gère un pointeur d’enregistrement déterminant sur quel enregistrement vont intervenir
les méthodes exécutées sur le ResultSet lui-même. Cet enregistrement est parfois appelé
enregistrement actif ou enregistrement courant. L’objet ResultSet contient toujours deux
enregistrements fictifs servant de repère pour le début du ResultSet (BOF - Beginning Of File) et pour la
fin du ResultSet (EOF - End Of File). Le pointeur d’enregistrement peut être positionné sur l’un de ces
deux enregistrements mais jamais avant l’enregistrement BOF ni après l’enregistrement EOF. Ces
enregistrements ne contiennent pas de données et une opération de lecture ou d’écriture sur ces
enregistrements déclenche une exception. À la création du ResultSet le pointeur est positionné avant le
premier enregistrement (BOF). Lorsque le jeu d’enregistrements ne contient aucun résultat, il y a
forcément ces deux enregistrements fictifs.
ˇ
boolean absolute(int position) : déplace le pointeur d’enregistrement sur
l’enregistrement spécifié. La numérotation des enregistrements débute à 1. Si la valeur de
l’argument position est négative, le déplacement est effectué en partant de la fin du
ResultSet . Si le numéro d’enregistrement n’existe pas le pointeur est positionné sur
l’enregistrement BOF si la valeur est négative et inférieure au nombre d’enregistrements ou sur
l’enregistrement EOF si la valeur est positive et supérieure au nombre d’enregistrements. Cette
méthode retourne true si le pointeur est positionné sur un enregistrement valide et false
dans le cas contraire (BOF ou EOF).
ˇ
boolean relative(int deplacement) : déplace le ponteur du nombre
d’enregistrements spécifié par l’argument deplacement . Si la valeur de cet argument est
positive le curseur descend dans le jeu d’enregistrements et si la valeur est négative le curseur
remonte dans le jeu d’enregistrements. Cette méthode retourne true si le pointeur est
positionné sur un enregistrement valide et false dans le cas contraire (BOF ou EOF).
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 42 -
Samuel KAMDOUM FOLONG
ˇ
void beforeFirst() : déplace le pointeur d’enregistrements avant le premier
enregistrement (BOF).
ˇ
void afterLast() : déplace le pointeur d’enregistrements après le dernier enregistrement
(EOF).
ˇ
boolean first() : déplace le pointeur d’enregistrements sur le premier enregistrement.
Cette méthode retourne true s’il y un enregistrement dans le ResultSet et false dans le
cas contraire.
ˇ
boolean last() : déplace le pointeur d’enregistrements sur le dernier enregistrement.
Cette méthode retourne true s’il y un enregistrement dans le ResultSet et false dans le
cas contraire.
ˇ
boolean next() : déplace le pointeur d’enregistrements sur l’enregistrement suivant
l’enregistrement courant. Cette méthode retourne true si le pointeur est sur un
enregistrement valide et false dans le cas contraire (EOF).
ˇ
boolean previous() : déplace le pointeur d’enregistrements sur l’enregistrement
précédant l’enregistrement courant. Cette méthode retourne true si le pointeur est sur un
enregistrement valide et false dans le cas contraire (BOF).
Pour toutes ces méthodes sauf pour la méthode next , il faut obligatoirement que le ResultSet soit de
type SCROLL_SENSITIVE ou SCROLL_INSENSITIVE . Si le ResultSet est de type
FORWARD_ONLY , seule la méthode next fonctionne et dans ce cas les autres méthodes déclenchent
une exception. Les méthodes suivantes permettent de tester la position du pointeur d’enregistrements :
ˇ
boolean isBeforeFirst() : retourne true si le pointeur est placé avant le premier
enregistrement (BOF).
ˇ
boolean isAfterLast() : retourne true si le pointeur est placé après le dernier
enregistrement (EOF).
ˇ
boolean isFirst() : retourne true si le pointeur est placé sur le
premier enregistrement.
ˇ
boolean isLast() : retourne true si le pointeur est placé sur le dernier
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 43 -
Samuel KAMDOUM FOLONG
enregistrement.
ˇ
int getRow() : retourne le numéro de l’enregistrement sur lequel se trouve le pointeur
d’enregistrements. La valeur 0 est retournée s’il n’y a pas d’enregistrement courant (BOF ou
EOF).
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
afficherPositionPointeur(rs);
// Tant qu'il y a des enregistrements
while ([Link]()) {
afficherPositionPointeur(rs);
}
afficherPositionPointeur(rs);
} catch (SQLException e) {
[Link]();
}
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 44 -
Samuel KAMDOUM FOLONG
try {
if ([Link]()) {
[Link]("le pointeur
est avant le premier enregistrement");
}
if ([Link]()) {
[Link]("le pointeur est
après le dernier enregistrement");
}
if ([Link]()) {
[Link]("le pointeur est
sur le premier enregistrement");
}
if ([Link]()) {
[Link]("le pointeur est
sur le dernier enregistrement");
}
int position;
position = [Link]();
if (position != 0 ) {
[Link]("c'est
l'enregistrement numéro " + position);
}
} catch (SQLException e) {
[Link]();
}
}
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 45 -
Samuel KAMDOUM FOLONG
L’objet ResultSet fournit de nombreuses méthodes permettant la lecture des champs d’un
enregistrement. Chacune de ces méthodes est spécifique à un type de données SQL. Il faut bien sûr
utiliser en priorité la méthode adaptée au type du champ dont on souhaite obtenir la valeur. Cependant
certaines de ces méthodes sont relativement souples et permettent la lecture de plusieurs types de
données. Le tableau ci-après reprend les principaux types de données SQL et les méthodes en permettant
la lecture à partir d’un ResultSet . Les méthodes marquées avec le symbole sont les méthodes
conseillées. Les méthodes marquées avec le symbole sont possibles mais avec des risques de
perte d’informations.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 46 -
Samuel KAMDOUM FOLONG
Chacune de ces méthodes est disponible sous deux formes. La première accepte comme argument le
numéro de la colonne dont on souhaite obtenir la valeur. La numérotation commençant à 1. La deuxième
version accepte une chaîne de caractères représentant le nom de la colonne dans la base de données. Si
la requête ayant été utilisée pour créer le ResultSet contient des alias alors les colonnes portent le
nom de l’alias et non le nom du champ dans la base de données. Pour une meilleure lisibilité du code, il
est bien sûr préférable d’utiliser les noms des colonnes plutôt que leurs indices. Lorsque dans la base de
données un champ ne contient pas de valeur (NULL ), les méthodes retournent une valeur égale à 0 pour
les champs numériques, une valeur false pour les champs booléens et une valeur null pour les autres
types. Dans certains cas, il y a donc un doute possible sur la valeur réellement présente dans la base. Par
exemple, la méthode getInt peut retourner une valeur égale à zéro parce qu’il y a effectivement cette
valeur dans la
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 47 -
Samuel KAMDOUM FOLONG
base de données ou parce que ce champ n’est pas renseigné dans la base de données.
Pour lever ce doute, la méthode wasNull retourne un boolean égal à true si le champ sur lequel la
dernière opération de lecture dans le ResultSet contenait effectivement une valeur null .
Le code suivant met en évidence la manipulation des types Java et SQL dans l’écriture et la lecture
d’informations. Un accent est mis sur la manipulation des dates.
Pour écrire une date dans une requête paramétrée, il est possible d’utiliser la méthode setDate qui
attend en deuxième paramètre un objet de type [Link] . Un objet de ce type ne doit pas être
manipulé dans le reste de l’application. Il est préférable d’utiliser un objet de type LocalDate (si la partie
horaire n’est pas utile) ou un objet de type LocalTime . La méthode [Link]
attend en paramètre un objet de type LocalDate pour obtenir un objet de type [Link] .
Pour écrire une date dans une requête paramétrée, il est aussi possible d’utiliser la méthode setObject
qui accepte en deuxième paramètre un objet. Cet objet peut être du type LocalDate . Le pilote JDBC
saura exploiter cet objet pour l’enregistrer dans une colonne de type Date .
Pour lire les données, les méthodes getXxx de l’interface ResultSet sont adaptées. La lecture d’une
date est réalisée avec la méthode getDate . Elle retourne un objet de type [Link] . Il est
possible d’obtenir un objet de type LocalDate à partir d’un objet de type [Link] grâce à la
méthode toLocalDate .
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 48 -
Samuel KAMDOUM FOLONG
import [Link];
import [Link];
//Exécuter la requête
int resultat = [Link]();
if(resultat==1 )
{
[Link]("L'insertion a réussi");
}
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 49 -
Samuel KAMDOUM FOLONG
//Exécuter la requête
resultat = [Link]();
if(resultat==1 )
{
[Link]("L'insertion a réussi");
}
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 50 -
Samuel KAMDOUM FOLONG
La modification des données est effectuée simplement en utilisant les méthodes updateXxx , où Xxx
correspond au type de données de la colonne à mettre à jour. Comme pour les méthodes getXxx , celles-
ci sont disponibles en deux versions, l’une attend comme argument l’indice de la colonne à mettre à jour,
la deuxième attend comme argument une chaîne de caractères représentant le nom de la colonne dans la
base de données. Si la requête ayant été utilisée pour créer le ResultSet contient des alias, alors les
colonnes portent le nom de l’alias et non le nom du champ dans la base de données. Pour une meilleure
lisibilité du code, il est bien sûr préférable d’utiliser les noms des colonnes plutôt que leurs indices.
Le type du deuxième argument attendu par ces méthodes correspond bien sûr au type de données à
mettre à jour dans le ResultSet . Les modifications doivent ensuite être validées par la méthode
updateRow ou annulées par la méthode cancelRowUpdates . Le ResultSet doit obligatoirement
être de type
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 51 -
Samuel KAMDOUM FOLONG
CONCUR_UPDATABLE pour pouvoir être modifié. Dans le cas contraire, l’exécution d’une de ces
méthodes déclenche une exception.
L’exemple suivant met à jour le nom des personnes en utilisant un ResultSet . Bien entendu, dans un
contexte professionnel, il serait plus judicieux d’utiliser une instruction SQL pour réaliser cette tâche.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 52 -
Samuel KAMDOUM FOLONG
[Link]();
}
}
}
private static Connection getConnexion() throws SQLException {
String url = "jdbc:mysql://localhost/demo_java?serverTimezone=UTC";
String user = "mysqluser";
String password = "Pa$$w0rd";
return [Link](url, user, password);
}
}
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 53 -
Samuel KAMDOUM FOLONG
La suppression d’une ligne est effectuée très simplement en positionnant le pointeur sur la ligne à
supprimer puis en appelant la méthode deleteRow . La ligne est immédiatement supprimée du
ResultSet et dans la base de données. La position du pointeur d’enregistrement après la suppression
dépend du pilote de base de données utilisé. Certains pilotes déplacent le pointeur sur l’enregistrement
suivant, d’autres le déplacent sur l’enregistrement précédent et enfin quelques-uns ne modifient pas la
position du pointeur. Il faut dans ce cas utiliser une des méthodes de déplacement pour positionner le
pointeur sur un enregistrement utilisable. Le ResultSet doit obligatoirement être de type
CONCUR_UPDATABLE pour pouvoir y supprimer des données. Dans le cas contraire, l’exécution de la
méthode deleteRow déclenche une exception.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 54 -
Samuel KAMDOUM FOLONG
ResultSet.CONCUR_UPDATABLE);
Statement stmLecture = [Link]()) {
lireEtSupprimerPersonnes(stmSuppression);
lirePersonnes(stmLecture);
} catch (SQLException e) {
[Link]();
}
}
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 55 -
Samuel KAMDOUM FOLONG
[Link]([Link]()?"inconnu":nom);
[Link]( "\t\t");
}
private static Connection getConnexion() throws SQLException {
String url =
"jdbc:mysql://localhost/demo_java?serverTimezone=UTC";
String user = "mysqluser";
String password = "Pa$$w0rd";
return [Link](url, user, password);
}
}
Ce résultat permet de conclure que le pilote JDBC pour MySQL ne modifie pas la position du curseur après
la suppression d’un enregistrement.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 56 -
Samuel KAMDOUM FOLONG
Chaque objet ResultSet contient une ligne spéciale destinée à l’insertion de données. Le pointeur
d’enregistrement doit au préalable être positionné sur cette ligne spéciale avec l’instruction
moveToInsertRow . Une fois que le pointeur est positionné sur cette ligne, celle-ci peut être mise à jour
avec les méthodes updateXXX . L’insertion de la ligne doit ensuite être validée avec la méthode
insertRow . Cette méthode provoque la mise à jour de la base de données. La ligne d’insertion devient à
ce moment une ligne normale du ResultSet et le pointeur d’enregistrement est positionné sur cette
ligne. Vous pouvez revenir sur la ligne sur laquelle vous étiez avant l’insertion grâce à la méthode
moveToCurrentRow . Si vous ne fournissez pas de valeur pour toutes les colonnes, des valeurs null
seront insérées dans la base de données pour les colonnes non renseignées. La base de données doit
accepter les valeurs nulles pour ces champs sinon une exception est déclenchée. Le ResultSet doit
obligatoirement être de type CONCUR_UPDATABLE pour pouvoir y insérer des données. Dans le cas
contraire, l’exécution de la méthode moveToInsertRow déclenche une exception.
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 57 -
Samuel KAMDOUM FOLONG
lirePersonnes(stmLecture);
} catch (SQLException e) {
[Link]();
}
}
private static void ajouterPersonne(Statement stmInsertion)
throws SQLException {
ResultSet rs = [Link]("select id, nom,
prenom, dateDeNaissance from PERSONNES");
[Link]();
[Link]("nom","DUJARDIN");
[Link]("prenom","Jean");
[Link]();
}
}
private static Connection getConnexion() throws SQLException {
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 58 -
Samuel KAMDOUM FOLONG
String url =
"jdbc:mysql://localhost/demo_java?serverTimezone=UTC";
String user = "mysqluser";
String password = "Pa$$w0rd";
return [Link](url, user, password);
}
}
Les transactions vont permettre de garantir qu’un ensemble d’instructions SQL sera exécuté avec succès.
En cas d’échec pour l’une d’entre elles, les instructions réalisées avec succès au préalable sont tout
simplement annulées. Ceci permet de maintenir cohérentes les informations présentes dans la base de
données. Le transfert d’une somme d’argent entre deux comptes bancaires représente l’exemple classique
où une transaction est nécessaire. Imaginez la situation suivante : notre banque doit effectuer
l’encaissement d’un chèque de 1000 € à débiter sur le compte numéro 12345 et à créditer sur le compte
67890. Par mesure de sécurité après chaque opération effectuée sur un compte (débit ou crédit), un
rapport est édité. Voici ci-dessous un extrait du code pouvant effectuer ces opérations.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 59 -
Samuel KAMDOUM FOLONG
Connection cnx=...
PreparedStatement stm;
stm=[Link]("update comptes set solde=solde
+ ? where numero=?");
[Link](1 ,somme * -1);
[Link](2 ,compteDebit);
[Link]();
impressionRapport(compteDebit, somme);
[Link](1 ,somme);
[Link](2 ,compteCredit);
[Link]();
impressionRapport(compteCredit, somme);
}
catch ( Exception e)
{
[Link]();
}
}
Pour 99,9999 % des mouvements effectués avec ce code, il n’y a aucun problème, mais un beau jour
l’imprimante chargée des éditions se bloque et ce blocage déclenche une exception dans la méthode
impressionRapport . A priori cette exception ne pose pas de problème puisque l’appel de cette
méthode est placé dans un bloc try et qu’un bloc catch traite l’exception. Il ne faut cependant pas
oublier que si une exception est déclenchée et qu’un bloc catch est exécuté, l’exécution se poursuit par
l’instruction suivant le bloc catch . Dans ce cas de figure, les instructions placées entre celle ayant
déclenché l’exception et la fin du bloc try ne sont tout simplement pas exécutées. Dans notre cas, ceci
peut être très problématique si cette exception est déclenchée dans la méthode impressionRapport
exécutée aussitôt après l’opération de débit. L’opération de crédit n’a tout simplement pas lieu. La somme
est donc perdue. Une solution consiste à annuler les instructions SQL ayant déjà été exécutées.
C’est ce mécanisme qui est mis en œuvre dans une transaction mais bien sûr de manière automatique.
C’est au niveau de la connexion vers le serveur de base de données que sont
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 60 -
Samuel KAMDOUM FOLONG
Jusqu’à présent, nous ne nous sommes pas préoccupés des transactions et pourtant nous en réalisons
depuis notre première instruction SQL exécutée via JDBC. Le fonctionnement par défaut de l’API JDBC
consiste effectivement à inclure chaque instruction exécutée dans une transaction puis valider la
transaction si l’instruction a été exécutée correctement (commit ) ou à annuler l’instruction dans le cas
contraire (rollBack ). Ce mode de fonctionnement est appelé mode autoCommit . Si vous souhaitez
gérer vous-même la fin d’une transaction en validant ou en annulant toutes les instructions qu’elle
contient, vous devez désactiver le mode autoCommit en appelant la méthode
setAutoCommit(false) sur l’objet Connection . Vous êtes maintenant responsables de la fin des
transactions. Les méthodes commit et rollback de l’objet Connection permettent de valider ou
d’annuler les instructions exécutées depuis le début de la transaction. Une nouvelle transaction débute
automatiquement dès la fin de la précédente ou dès l’ouverture de la connexion. Le code permettant de
transférer un montant entre deux comptes doit donc prendre la forme suivante.
Pour que le programme suivant fonctionne, il est nécessaire de créer une table avec un jeu d’essai. Voici le
script de création de la table COMPTES et d’ajout de deux comptes :
À présent que la base de données est configurée, le code suivant permet de mettre en évidence le
fonctionnement des transactions :
package [Link];
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 61 -
Samuel KAMDOUM FOLONG
import [Link];
import [Link];
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 62 -
Samuel KAMDOUM FOLONG
}
}
les instructions SQL sont annulées et le contenu de la base de données n’est pas modifié.
b. Points de sauvegarde
Lors de l’appel de la méthode rollback , l’ensemble des instructions SQL exécutées depuis le début de
la transaction sont annulées. Cette méthode propose une deuxième version acceptant comme paramètre
un objet SavePoint . Cet objet représente un repère dans l’exécution des instructions SQL. Il est créé par
la méthode setSavePoint . L’appel de la méthode rollback avec comme argument un objet
SavePoint provoque
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 63 -
Samuel KAMDOUM FOLONG
c. Niveaux d’isolement
Pendant qu’une transaction est active, les données modifiées par les instructions exécutées au sein de la
transaction peuvent être verrouillées par la base de données pour éviter les conflits et les incohérences.
Différents types de verrous sont disponibles pour une transaction. Pour bien comprendre leurs effets, il
faut au préalable identifier les types de problèmes pouvant être rencontrés lorsqu’une transaction est
active.
ˇ Lecture erronée : cette anomalie se produit lorsqu’une application accède à des données qui
sont en train d’être modifiées par une transaction qui n’a pas encore été validée.
ˇ Lecture non reproductible : cette anomalie se produit lorsque les exécutions successives d’une
même instruction select ne produisent pas le même résultat. C’est le cas si les données que
vous lisez sont en cours de modification par une autre transaction.
ˇ Lecture fantôme : cette anomalie se produit si des exécutions successives d’une même
requête renvoient des données en plus ou en moins. Cela peut être le cas si une autre
transaction est en train de supprimer ou d’ajouter des données à la table.
L’API JDBC prévoit plusieurs niveaux d’isolement. Ils déterminent la manière dont sont verrouillées les
données durant la transaction. Ce verrouillage peut être placé en lecture, en écriture ou encore en lecture
et en écriture sur les données accédées par les instructions de la transaction.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 64 -
Samuel KAMDOUM FOLONG
Le fait de spécifier un niveau d’isolement pour une transaction peut parfois avoir un effet sur les autres
applications accédant aux données manipulées dans la transaction puisque celles-ci peuvent être
verrouillées et donc être inaccessibles aux autres applications.
© Editions ENI - Tous droits réservés - Copie personnelle de Samuel KAMDOUM FOLONG - 65 -