Un objet de type Statement
s'obtient en appelant la méthode createStatement()
de l'interface Connection
.
On peut créer autant d'objets de ce type par connexion, et tous ces objets peuvent être utilisés de façon concurrente.
On peut de plus passer trois paramètres de configuration lors de la création d'une instance de
Statement
.
resultSetType
: un entier qui peut prendre les valeurs TYPE_FORWARD_ONLY
,
TYPE_SCROLL_INSENSITIVE
et TYPE_SCROLL_SENSITIVE
.
resultSetConcurrency
: un entier qui peut prendre les valeurs
CONCUR_READ_ONLY
et CONCUR_UPDATABLE
.
resultSetHoldability
: de même, un entier qui peut prendre les valeurs
HOLD_CURSORS_OVER_COMMIT
et CLOSE_CURSORS_AT_COMMIT
.
Toutes ces valeurs entières sont des entiers publics et statiques de la classe ResultSet
.
Nous verrons leur signification exacte dans le paragraphe sur les ResultSet
.
Il est important de fermer un Statement
lorsqu'il n'est plus utilisé, par appel à sa
méthode close()
.
Il existe trois méthodes pour exécuter un Statement
. Chacune de ces méthodes doit
être utilisée dans son propre contexte.
executeQuery(String)
doit être utilisée si l'exécution du Statement
retourne une liste d'objets (cas d'un select
) ;
executeUpdate(String)
doit être utilisée si l'exécution du Statement
retourne un nombre d'objets modifiés (cas des requête de création, d'effacement ou de mise à jour) ;
execute(String
doit être utilisée si le type de Statement
exécuté
n'est pas connu.
Ces trois méthodes prennent une chaîne de caractères en paramètres. Cette chaîne est la commande SQL qui va être exécutée sur le serveur.
Cette méthode retourne un objet de type ResultSet
. Si la requête SQL passée en paramètre
ne correspond pas (par exemple, si elle contient un insert
), alors une exception de type
SQLException
est jetée.
Toutes les requêtes de mise à jour de la base retournent un entier qui correspond aux nombres d'éléments créés, mis à jour ou effacés. Voyons comment on utilise une telle méthode.
Cette méthode doit être utilisée si l'on ne connait pas la nature de la requête SQL qui va être
effectivement exécutée. Elle retourne true
si l'objet retourné est de type
ResultSet
et false
s'il s'agit d'un entier. On peut ensuite obtenir
ces objets en appelant les méthodes getResultSet()
et getUpdateCount()
.
De plus, si l'exécution de cette requête a généré plusieurs résultats, la méthode
getMoreResult()
doit être appelée. Voyons l'utilisation de toute ceci.
Exemple 4.4. Méthode Statement.execute(String)
Statement stmt = conn.createStatement() ; boolean returnedValue = smt.execute(sql) ; // on ne connaît pas la nature // de cette requête ResultSet rs ; int count ; do { if (returnedValue) { // le résultat est un result set rs = stmt.getResultSet(); // exploitation du result set ... } else { // le résultat est un entier count = stmt.getUpdateCount() ; if (count == -1) { // une valeur -1 indique qu'il n'y a plus de résultat à exploiter break ; // sortie du while } else { // exploitation du count ... } } returnedValue = smt.getMoreResult() ; } while (true) ;
Notons que par défaut, l'ouverture d'un nouveau resultSet
par appel à la méthode
getResultSet()
ferme automatiquement l'ancien. Ce comportement peut être changé, si
le pilote utilisé le permet, comme nous le verrons dans le paragraphe sur les ResultSet
.
Tous les objets de type Statement
doivent être fermés par un appel à la méthode
close()
une fois qu'ils ne sont plus utilisés. La fermeture d'un Statement
entraîne automatiquement la fermeture de tous les objets ResultSet
qui ont été ouverts
sur ce Statement
.
Notons que même si la fermture d'une connexion (par appel à sa méthode close()
) ferme
automatiquement tous les Statement
ouverts dessus, il est recommandé de le faire à la main.
De même que pour la fermeture des ResultSet
.
Une fois qu'un objet Statement
ou ResultSet
a été fermé, il est illégal
d'invoquer une de ses méthodes. L'appel d'une telle méthode peut jeter systématiquement une exception
de type SQLException
.
L'interface PreparedStatement
étend l'interface Statement
, donc tout ce qui
précède et qui concerne l'interface Statement
reste valide pour l'interface
PreparedStatement
.
L'interface PreparedStatement
ajoute la possibilité de paramétrer des requêtes SQL.
Les instances de PreparedStatement
s'utilisent quand une même requête doit être
exécutée plusieurs fois, avec des paramètres différents. La chaîne de caractères SQL contient
donc des marqueurs, qui seront remplacés par des valeurs à chaque exécution.
On préfèrera systématiquement l'utilisation de PreparedStatement
aux Statement
classiques, en particulier pour les requêtes de mise à jour. Ces requêtes comportent le plus souvent
des paramètres, qu'il faut ajouter à la chaîne de caractères SQL à exécuter. Utiliser des
PreparedStatement
permet de passer par les méthodes de paramétrage du pilote de base,
ce qui nous décharge de la délicate gestion des erreurs, et évitera les attaques par injection de
code SQL.
Une instance de PreparedStatement
se crée de la même façon qu'un Statement
normal.
Exemple 4.5. Création d'un PreparedStatement
PreparedStatement ps = conn.prepareStatement( "insert into Marins (nom, prenom, age) values (?, ?, ?)") ;
De même que dans le cas d'un Statement
normal, on peut passer des options lors de la création
d'un PreparedStatement
.
On remarque la présence de trois caractères ?
dans la chaîne SQL passée en paramètre.
Ce sont ces caractères qui doivent être remplacés par des valeurs afin de pouvoir exécuter la requête
SQL proprement dite.
L'interface PreparedStatement
propose un jeu de méthode
set<Type>(int, Type)
:
setInt(int, int)
, setFloat(int, float)
, setString(int, String)
,
etc... Chacune de ces méthodes peut être appelée pour fixer la valeur d'un paramètre donné.
Exemple 4.6. Fixer les paramètres d'un PreparedStatement
PreparedStatement ps = conn.prepareStatement( "insert into Marins (nom, prenom, age) values (?, ?, ?)") ; ps.setString(1, "Surcouf") ; ps.setString(2, "Robert") ; ps.setInt(3, 32) ;
On notera que la numérotation des paramètres commence à 1. Bien sûr, tous les paramètres doivent
avoir une valeur fixée avant que la requête soit exécutée, sous peine de SQLException
.
On peut effacer la valeur de tous les paramètres d'un PreparedStatement
en appelant
la méthode clearParameter()
.
Positionner un paramètre à la valeur null
nécessite l'utilisation d'une méthode
particulière : setNull(int, int)
. Cette méthode prend deux paramètres :
le premier est le numéro d'ordre du paramètre ;
le second est le type du paramètre, il s'agit de l'une des constantes définies
dans la classe java.sql.Types
L'exécution d'un PreparedStatement
se déroule de la même manière que pour un
Statement
, à l'aide des trois méthodes executeQuery()
,
executeUpdate()
et execute()
.
L'interface PreparedStatement
propose deux méthodes :
getMetaData()
: retourne un objet ResultSetMetaData
, qui contient
des informations sur les colonnes retournées par la requête SQL ;
getParameterMetaData()
: retourne un objet ParameterMetaData
, qui
contient des informations sur les paramètres déclarés dans ce PreparedStatement
.
La méthode getMetaData()
retourne un objet de type ResultSetMetaData
qui
propose un jeu d'une vingtaine de méthodes pour déterminer le types de colonnes du résultat de
la requête. Voyons ceci sur un exemple.
Exemple 4.8. Utilisation d'un objet ResultSetMetaData
// on ne connaît pas les colonnes du résultat de cette requête PreparedStatement pstmt = conn.prepareStatement("select * from Marins where id = ?") ; ResultSetMetaData rsmd = pstmt.getMetaData() ; // lecture du nombre de colonne int columnCount = rsmd.geColumnCount() ; for (int i = 1 ; i <= columnCount ; i++) { int columnType = rsmd.getColumnType(i) ; // label de la colonne String columnLabel = rsmd.getColumnLabel(i) ; // nom de la colonne String columnName = rsmd.getColumnName(i) ; }
On remarquera qu'ici aussi les paramètres sont numérotés à partir de 1.
La méthode getParameterMetaData()
retourne un objet de type ParameterMetaData
,
qui lui-même propose
quelques méthodes pour obtenir des informations sur les paramètres d'un PreparedStatement
.
Voyons un exemple d'utilisation.
Exemple 4.9. Utilisation d'un objet ParameterMetaData
// on ne connaît pas les colonnes du résultat de cette requête PreparedStatement pstmt = conn.prepareStatement("select * from Marins where id = ?") ; ParameterMetaData pmd = pstmt.getParameterMetaData() ; // lecture du nombre de colonne int parameterCount = pmd.geParameterCount() ; for (int i = 1 ; i <= parameterCount ; i++) { // type du paramètre (une constante de java.sql.Types) int parameterType = pmd.getColumnType(i) ; // mode du paramètre int parameterMode = pmd.getParameterMode(i) ; }
Notons que le mode d'un paramètre, qui peut prendre l'une des valeurs IN
,
OUT
ou INOUT
est utilisé pour les CallableStatements
.
Lors de l'insertion de lignes dans une table, il arrive que certaines valeurs soient générées automatiquement par la base de données. C'est notamment le cas des colonnes auto-incrémentales que l'on peut utiliser pour générer des clés primaires dans les tables MySQL.
JDBC nous donne un moyen d'obtenir la valeur de la clé générée aussitôt la requête d'insertion effectuée. Heureusement, car sans ce moyen, il pourrait être assez compliqué de récupérer la donnée que l'on vient d'insérer. Voyons ceci sur un exemple.
Exemple 4.10. Méthode getGeneratedKeys()
Statement stmt = connection.createStatement() ; // le paramètre RETURN_GENERATED_KEYS va nous permettre de récupérer // la clé générée par MySQL int rowCount = smt.executeUpdate( "insert into Marins (nom, prenom) values ('Surcouf', 'Robert')", Statement.RETURN_GENERATED_KEYS) ; ResultSet rs = stmt.getGeneratedKeys(); if (rs.next()) { // récupération de la clé primaire int idSurcouf = rs.getInt(1) ; }
On notera que pour fonctionner correctement, cette méthode ne doit être appelée que si la transaction est en mode non auto-commit.
Il est également possible de grouper des requête SQL dans un Statement
et de les exécuter
en une seule fois. On appelle ce genre d'exécution l'exécution en batch. En général
cela signifie que l'on s'attend à ce que l'exécution totale soit assez longue, et que pendant ce temps,
le système peut faire autre chose.
L'exécution de commandes SQL en batch n'est possible que pour des requêtes de modification de la base,
celles que l'on exécute avec la méthode executeUpdate(String)
. Toute requête SQL qui peut
être exécutée de la sorte, peut également être passée en paramètre de la méthode
addBatch(String)
. L'appel de cette méthode ne fait que stocker la requête SQL, sans
l'exécuter. C'est l'appel à la méthode executeBatch()
qui déclenche l'exécution.
L'exécution en batch peut représenter des gains substantiels en performances. Le coût d'une aller et retour avec la base de données est en lui-même coûteux, et l'on tentera le plus possible de minimiser ces allers et retours.
Voyons un exemple d'exécution en batch pour les objets Statement
.
Exemple 4.11. Exécution d'un Statement
en batch
// l'exécution en batch doit se faire en mode non auto-commit connection.setAutoCommit(false) ; Statement smt = connection.createStatement() ; smt.addBatch("insert into Marins(nom, prenom) values ('Surcouf', 'Robert')) ; smt.addBatch("insert into Marins(nom, prenom) values ('Tabarly', 'Eric')) ; ... // lancement de l'exécution de toutes nos insertions int [] updateCounts = smt.executeBatch() ;
Comme le mode auto-commit a été désactivé, il nous est possible, en cas d'erreur
sur l'appel à executeBatch()
d'annuler la transaction et de remettre la base dans l'état
initial.
Il est aussi possible de lancer des exécutions de mise à jour en batch lorsque l'on travaille avec
des PreparedStatement
. Le mécanisme est toutefois légèrement différent, puisqu'un
PreparedStatement
est construit sur une unique requête SQL paramétrée.
PreparedStatement
propose une méthode addBatch()
qui ne prend aucun paramètre.
Au moment où cette méthode est appelée, JDBC construit une requête SQL avec la requête du
PreparedStatement
, et le jeu de paramètres qu'il a à sa disposition. Si l'un des paramètres
n'a pas été fixé, une erreur est générée. On peut ainsi ajouter autant de requêtes que l'on veut.
L'exécution des requêtes se fait de la même manière et sous les mêmes contraintes que pour un
Statement
classique.
Exemple 4.12. Exécution d'un PreparedStatement
en batch
// l'exécution en batch doit se faire en mode non auto-commit connection.setAutoCommit(false) ; PreparedStatement psmt = connection.prepareStatement("insert into Marins(nom, prenom) " + "values (?, ?)") ; psmt.setParameter(1, "Surcouf") ; psmt.setParameter(2, "Robert") ; // ajout d'une requête avec les paramètres ('Surcouf', 'Robert') psmt.addBatch() ; psmt.setParameter(2, "Pierre") ; // ajout d'une requête avec les paramètres ('Surcouf', 'Pierre') psmt.addBatch() ; psmt.setParameter(1, "Tabarly") ; psmt.setParameter(2, "Eric") ; // ajout d'une requête avec les paramètres ('Tabarly', 'Eric') psmt.addBatch() ; ... // lancement de l'exécution de toutes nos insertions int [] updateCounts = smt.executeBatch() ;