Pour comprendre ce qui suit, il faut s'imaginer qu'un ResultSet
est en fait un tableau
dont les colonnes sont celles qui ont été extraites par notre requête SQL, et dont les lignes sont
les résultats de cette requête. Examiner les résultats de notre requête consiste donc à examiner
le contenu de ce tableau, ligne par ligne. La ligne courante est désignée par un
curseur, dont il va être question dans la suite.
Le type d'un ResultSet
recouvre deux choses : les mouvements possibles pour le curseur,
et la sensibilité du résultat à la base de données sous-jacente. Effectivement, la spécification JDBC
ne précise pas si le tableau de résultat doit être lu en une fois, au moment de l'exécution de la
requête, ou par paquets, au fur et à mesure que l'on balaye ses lignes. Dans le deuxième cas, la question
peut se poser : est-on en train de lire la base telle qu'elle était au moment de la requête, ou bien
ce que l'on voit prend-il en compte des modifications qui pourraient avoir eu lieu par ailleurs ?
Il y a trois types de ResultSet
, définis par des constantes de cette interface :
TYPE_FORWARD_ONLY
: c'est le mode par défaut. Le curseur ne peut se déplacer que
d'une ligne à la fois, et uniquement vers l'avant. Le mode de
sensibilité n'est pas défini, dépend de la base de données que l'on utilise, et de son pilote.
TYPE_SCROLL_INSENSITIVE
: signifie que tous les mouvements du curseur sont possibles, y
compris son positionnement sur une ligne directement. Le mode insensible
signifie que des modifications faites à la base ne sont pas transmises au ResultSet
,
ce qui signifie que la lecture plusieurs fois d'une même ligne renverra toujours le même résultat.
En revanche, on ne sait pas si les données sont lues au moment de l'exécution de la commande SQL,
ou au moment de la lecture d'une ligne.
TYPE_SCROLL_SENSITIVE
: de même, tous les mouvements du curseur sont possibles. Les
changements faits sur la base de données sont reflétés dans le ResultSet
. Donc deux
lectures successives d'une même ligne peuvent donner des résultats différents.
Le type de ResultSet
que l'on veut peut être imposé lors de la création d'un
Statement
, en fixant son paramètre resultSetType
. On peut tester notre
connexion afin de savoir si elle supporte le type de ResultSet
que l'on veut,
comme dans l'exemple suivant.
Exemple 5.1. Fixer le type d'un ResultSet
, utilisation de DatabaseMetaData
// récupération des information sur notre base DataBaseMetaData dbdm = connection.getMetaData() ; // notre base supporte-t-elle le mode TYPE_SCROLL_SENSITIVE ? if (dbdm.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE)) { // traitement PreparedStatement psmt = connection.prepareStatement( "select Marins where nom like ?", ResultSet.TYPE_SCROLL_SENSITIVE) ; ... }
Deux valeurs sont disponibles sur resultSetConcurrency
:
CONCUR_READ_ONLY
: aucune mise à jour des données n'est possible au travers de
ce ResultSet
. Il s'agit du mode par défaut.
CONCUR_UPDATABLE
: il est possible de mettre à jour les données de ce
ResultSet
.
Là encore, tous les pilotes ne supportent pas nécessairement la mise à jour des données au travers
du ResultSet
. On peut interroger les méta-données de la base de la même façon que pour
le type de la façon suivante.
Exemple 5.2. Fixer la concurrence d'un ResultSet
// récupération des information sur notre base DataBaseMetaData dbdm = connection.getMetaData() ; // notre base supporte-t-elle le mode CONCUR_UPDATABLE ? if (dbdm.supportsResultSetConcurrency(ResultSet.CONCUR_UPDATABLE)) { // traitement PreparedStatement psmt = connection.prepareStatement( "select Marins where nom like ?", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE) ; ... }
Lorque la méthode commit()
est appelée sur la transaction, manuellement ou
automatiquement, il se peut que les ResultSet
ouverts sur cette connexion soient
également fermés, ce qui n'est pas toujours le résultat désiré. Après tout, si ces ResultSet
sont ouverts en lecture seule, rien ne devrait pouvoir s'oppsoser à ce que l'on puisse continuer à
lire les données qu'ils contiennent.
Là encore deux valeurs sont possibles pour le paramère resultSetHoldability
:
HOLD_CURSORS_OVER_COMMIT
: les curseurs des ResultSet
sont conservés
lors d'un commit()
;
CLOSE_CURSORS_AT_COMMIT
: les curseurs sont fermés lors d'un commit()
.
Ici la norme JDBC ne définit pas de comportement par défaut, il faut donc interroger la connexion afin de savoir dans quel mode on se trouve.
Exemple 5.3. Fixer la concurrence d'un ResultSet
// récupération des information sur notre base DataBaseMetaData dbdm = connection.getMetaData() ; // notre base supporte-t-elle le mode CONCUR_UPDATABLE ? if (dbdm.getResultSetHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT)) { // traitement PreparedStatement psmt = connection.prepareStatement( "select Marins where nom like ?", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE, ResultSet.HOLD_CURSORS_OVER_COMMIT) ; ... }
On obtient un objet ResultSet
le plus souvent en invoquant la méthode
executeQuery(String)
d'un objet Statement
.
Un ResultSet
peut être vu comme un tableau de résultats, dont chaque colonne est un
champ, et chaque ligne un enregistrement.
La lecture des lignes d'un ResultSet
se fait au travers d'un curseur, que l'on peut
déplacer ligne par ligne. Lors que l'on obtient un objet ResultSet
, par exécution de
la méthode executeQuery(String)
, ce curseur est positionné sur une ligne virtuelle,
qui se trouve avant la première ligne du tableau.
La première catégorie de méthodes proposées par l'interface ResultSet
va donc
nous permettre de déplacer le curseur dans ce tableau.
Voici ces méthodes.
next()
: déplace le curseur sur la ligne suivante. Elle retourne
true
si le curseur est positionné sur une ligne, false
si le curseur
a dépassé la fin du tableau. La valeur du retour de cette méthode doit obligatoirement être
testée avant la lecture de la ligne courante.
previous()
: déplace le curseur sur la ligne précédente. Retourne true
ou false
, suivant que le curseur se positionne sur une ligne, ou en dehors du
tableau.
first()
, last()
: positionne le curseur sur la première ligne ou
la dernière ligne du tableau. Retourne true
ou false
suivant que
cette ligne existe ou pas.
beforeFirst()
, afterLast()
: positionne le curseur sur l'une des
lignes virtuelles se trouvant avant la première ligne, ou après la dernière. Ce mouvement
est toujours possible, même sur un tableau vide.
relative(int rows)
: déplace le curseur du nombre de lignes indiqué. Le déplacement
a lieu vers le bas du tableau si ce nombre est positif, vers le haut s'il est négatif. Si
rows
vaut 0, alors le curseur ne bouge pas. Si le tableau ne comporte pas assez de
ligne, vers le haut ou vers le bas, alors le curseur se positionne sur l'une des ligne virtuelle
avant la première ligne, ou après la dernière. Cette méthode retourne true
si le
curseur a été positionné sur une ligne, false
dans le cas contraire.
absolute(int rows)
: positionne le curseur en absolu dans le tableau. La première
ligne du tableau est numérotée 1, donc absolute(1)
positionne le curseur sur
cette première ligne. On peut passer un paramètre négatif à cette méthode. Dans ce cas, les
lignes sont numérotées à partir de la dernière, et en commençant par -1. Ainsi
absolute(-1)
positionne le curseur sur la denière ligne du tableau,
absolute(-2)
sur l'avant-dernière etc... Si rows
est plus grand
que le nombre de lignes, alors le curseur se positionne sur la ligne virtuelle qui se
trouve après le tableau si rows
est positif, et sur la ligne virtuelle
avant le tableau s'il est négatif. L'appel à absolute(0)
positionne le curseur
avant la première ligne du tableau.
Si notre ResultSet
est de type FORWARD_ONLY
, alors la seule méthode
possible est next()
.
Une fois le curseur positionné sur une ligne valide, il est possible d'accéder aux valeurs
de chaque cellule. Pour cela, ResultSet
expose une collection de méthode
de type get<Type>()
, qui permet de lire la valeur de chaque cellule
dans son bon type.
Ces méthodes peuvent prendre deux types de paramètre :
Un entier de type int
, qui doit correspondre au numéro de la colonne que l'on
cherche. Les colonnes sont numérotées à partir de 1.
Une chaîne de caractères String
, qui doit correspondre au nom de la colonne.
Notons que l'on peut obtenir le numéro d'une colonne par la méthode findColumn(String)
de ResultSet
.
Exemple 5.4. Lecture du résultat d'une requête
// création d'un prepared statement sur une requête simple PreparedStatement psmt = connection.prepareStatement("select * from Marins where id = ?") ; // exécution de la requête psmt.setInt(1, 15) ; ResultSet rs = psmt.executeQuery() ; // analyse du résultat ResultSetMetaData md = rs.getMetaData() ; int columnCount = md.getColumnCount() ; // lecture du résultat if (rs.next()) { for (int columnIndex = 1 ; columnIndex <= columnCount ; columnCount++) { // lecture du type de notre colonne int columnType = md.getColumnType(columnIndex) ; if (columnType == java.sql.Type.INT) { String columnName = md.getColumnName(columnIndex) ; int value = rs.getInt(columnIndex) ; } else if (columnType == java.sql.Type.STRING) { String columnName = md.getColumnName(columnIndex) ; String value = rs.getString(columnIndex) ; } // on peut ainsi balayer tous les types de notre table } }
Lors de la lecture des valeurs de notre ligne, une conversion est effectuée du type SQL vers
le type Java associé. Un problème se pose pour les int
(par exemple), puisqu'un
int
en Java est un type de base, qui ne peut pas être nul. En fait, un entier nul
en SQL vaudra 0 en Java, et un booléen vaudra false
.
Dans ce cas, on peut appeler la méthode wasNull()
, qui ne prend pas d'argument, et
qui nous retourne true
si la dernière lecture qui a été faite, était en fait une
valeur nulle.
La lecture des BLOB
SQL suit un processus différent, simplement parce qu'un BLOB
correspond en général à une quantité importante de données, et qu'il ne serait pas toujours possible,
ou du moins trop coûteux de la stocker entièrement en mémoire.
Plutôt que de retourner la valeur directement au code appelant, comme c'est le cas pour les entiers
ou les chaînes de caractères, le pilote de base de données nous retourne un pointeur directement
vers le BLOB
, sur lequel il est possible d'ouvrir un flux (stream).
La méthode getBlob()
retourne donc un objet de type Blob
. Cet objet comporte
une méthode getBinaryStream()
qui nous retourne un objet de type InputStream
.
C'est à partir de cet objet que l'on peut exploiter le contenu de notre blob
.
On pourra se reporter à la documentation de l'interface java.sql.Blob
pour plus de détails.
Il existe trois types de date en SQL : DATE
, TIME
et TIMESTAMP
.
Ces trois types sont associés à trois types Java : java.sql.Date
,
java.sql.Time
et java.sql.Timestamp
. Le piège est qu'il existe aussi une classe
Java java.util.Date
, étendue par java.sql.Date
, et qu'il ne faut pas
utiliser dans le contexte de JDBC.
La mise à jour d'une base de données direcement au travers d'un ResultSet
est possible
si le resultSetConcurrency
est de type CONCUR_UPDATABLE
. Dans ce cas,
trois types d'opérations sont possibles :
la modification des champs d'une ligne ;
l'ajout d'une ligne ;
la suppression d'une ligne.
Il est possible de modifier la ligne courante d'un ResultSet
en utilisant une des
méthodes update<Type>(int, Type)
, qui prend en paramètre le numéro de la colonne
et la nouvelle valeur à attribuer à cette colonne pour cette ligne.
On peut de cette façon modifier autant de colonnes que l'on souhaite sur la ligne courante.
La modification sera prise en compte sur l'appel à la méthode updateRow()
de
ResultSet
. On peut également annuler ces par l'appel à la méthode
cancelRowUpdates()
Exemple 5.6. Mise à jour d'un ResultSet
// création d'un statement PreparedStatement psmt = connection.prepareStatement( "select nom, prenom, ddnaissance from Marins where nom = ?") ; // exécution de la requête psmt.setString(1, "Tabarly") ; ResultSet rs = psmt.executeQuery() ; // mise à jour du résultat if (rs.next()) { // modification du resultset rs.updateString("prenom", "Eric") ; rs.updateInt("ddnaissance", 1931) ; // prise en compte rs.updateRow() ; }
Notons qu'un ResultSet
ne "voit" pas nécessairement les modifications que l'on fait
sur lui. Cela signifie que si l'on lit la valeur d'une cellule que l'on vient de modifier, il
n'est pas garanti que l'on lise la valeur modifiée, quand bien même elle a été correctement
prise en compte.
On peut interroger la méthode DatabaseMetaData.ownUpdatesAreVisible(int type)
afin de savoir si un ResultSet
voit les modifications qu'on lui applique, pour
un type (java.sql.Types
) donné.
Exemple 5.7. Tester si une modification est visible dans un ResultSet
DatabaseMetaData md = connection.getMetaData() ; // le type du result set peut valoir TYPE_FORWARD_ONLY, // TYPE_SCROLL_INSENSITIVE ou TYPE_SCROLL_SENSITIVE if (md.ownUpdatesAreVisible(resultSet.getType())) { // les modifications de ce result set sont visibles }
De même, un ResultSet
peut voir ou non les modifications faites dans d'autres
ResultSet
(et donc éventuellement dans d'autres transactions), et l'on peut
tester ce comportement
grâce à la méthode DatabaseMetaData.otherUpdatesAreVisible(int type)
La suppression de la ligne courante se fait simplement par appel à la méthode deleteRow()
.
On peut tester si notre ResultSet
voit ses propres suppressions ou pas, par appel
à la méthode DatabaseMetaData.ownDeletesAreVisible(int type)
. Si elles le sont, alors
la méthode rowDeleted()
retourne true
si la ligne a été effacée,
false
sinon. Il existe aussi une méthode
DatabaseMetaData.otherDeletesAreVisible(int type)
qui permet de savoir si les effacements
de lignes menées dans d'autres ResultSet
(et donc éventuellement dans d'autres
transactions) sont visibles ou non.
Il est important de savoir si un ResultSet
est sensible ou non aux lignes que l'on efface
dedans, car dans certaines implémentations, les lignes effacées ne sont pas retirées, mais
remplacées par des lignes vides.
Dans ce cas, il faut prendre garde à bien vérifier que l'on n'est pas sur une ligne fantôme
en appelant rowDeleted()
systématiquement sur chaque ligne avant d'analyser
son contenu.
On peut savoir si l'implémentation de JDBC que l'on utilise efface réellement les lignes d'un
ResultSet
ou les remplace par des lignes vides en appelant la méthode
deletesAreDetected()
de ResultSet
. Cette méthode renvoie true
si une ligne effacée est remplacée par une ligne vide ou invalide.
La création d'une ligne dans un ResultSet
est une option du standard, qui n'est
pas supportée par tous les pilotes.
L'insertion d'une ligne est un processus qui se déroule en trois temps :
Placer le curseur sur une ligne virtuelle particulière par appel à la méthode
moveToInsertRow()
.
Renseigner les valeurs de cette ligne pour toutes les colonnes, par les méthodes
update<Type>(Type)
.
Insérer cette nouvelle ligne dans le tableau par appel à la méthode insertRow()
.
Une fois notre nouvelle ligne insérée, on peut positionner le curseur dessus en appelant la méthode
moveToCurrentRow()
. Voyons ceci sur un exemple.
Exemple 5.8. Création d'une ligne dans un ResultSet
// création d'un result set ResultSet rs = psmt.executeQuery() ; // positionnement sur la ligne d'insertion rs.moveToInsertRow() ; // préciser la valeur de toutes les colonnes rs.updateString("nom", "Tabarly") ; rs.updateString("prenom", "Eric") ; rs.updateInt("ddnaissance", 1931) ; // valider la création de la ligne rs.insertRow() ; // positionnement du curseur sur la ligne nouvellement créée rs.moveToCurrentRow() ;
Là encore, c'est le résultat de l'appel aux méthodes
DatabaseMetaData.ownInsertsAreVisible(int type)
et
DatabaseMetaData.otherInsertsAreVisible(int type)
qui nous dira si le ResultSet
voit les modifications qu'il a lui-même faite
sur la base.
Là encore il faut tester sur le ResultSet
voit ses propres modifications ou celles des
autres, par les méthodes