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.
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 17. 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) ; ... }
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
.
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 18. 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) ; ... }
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()
.
Exemple 19. 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) ; ... }
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.
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.
ResultSet
est de type
FORWARD_ONLY
, alors la seule méthode possible est
next()
.
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 :
int
, qui doit correspondre au numéro de la colonne que l'on cherche. Les colonnes sont numérotées à partir de 1.
String
, qui doit correspondre au nom de la colonne.
findColumn(String)
de
ResultSet
.
Exemple 20. 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 } }
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.
Exemple 21. Utilisation de
rs.wasNull()
// reprenons le morceau de code de l'exemple précédent if (columnType == java.sql.Type.INT) { String columnName = md.getColumnName(columnIndex) ; int value = rs.getInt(columnIndex) ; if (rs.wasNull()) { // dans ce cas la valeur SQL de value est null } }
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.
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.
ResultSet
est possible si le
resultSetConcurrency
est de type
CONCUR_UPDATABLE
. Dans ce cas, trois types d'opérations sont possibles :
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 22. 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() ; }
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 23. 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 }
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)
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.
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 :
moveToInsertRow()
.
update<Type>(Type)
.
insertRow()
.
moveToCurrentRow()
. Voyons ceci sur un exemple.
Exemple 24. 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() ;
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
ResultSet
peut être (et doit être !) explicitement fermé par un appel à sa méthode
close()
. Une fois fermé, on ne peut plus accéder à son contenu. Notons que la fermeture d'un
ResultSet
n'entraîne pas
la fermeture des objets qui lui sont attachés, notamment les
BLOB
.