2. Types d'objets ResultSet

2.1. Types de ResultSet

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 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) ;
   ...

}

2.2. Mise à jour au travers d'un ResultSet : concurrency

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 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) ;
   ...

}

2.3. Comportement lors de la fermeture du Statement : holdability

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 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) ;
   ...

}

2.4. Lecture du résultat

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.

2.4.1. Déplacement du curseur d'un ResultSet

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().

2.4.2. Lecture des valeurs

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 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 &lt;= 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		
   }
}

2.4.3. Cas des valeurs nulles

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.

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
         }
      }

2.4.4. Lectude des données binaires ( BLOB)

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.

2.4.5. Remarque sur les dates

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.

2.5. Mise à jour du résultat

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.

2.5.1. Modification 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 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() ;
}

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 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
	
}

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)

2.5.2. Suppression d'une ligne

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.

2.5.3. Création d'une ligne

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 :
  1. Placer le curseur sur une ligne virtuelle particulière par appel à la méthode moveToInsertRow().
  2. Renseigner les valeurs de cette ligne pour toutes les colonnes, par les méthodes update<Type>(Type).
  3. 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 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() ;

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

2.6. Fermeture d'un ResultSet

Un 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.
Java Database Connectivity
Retour au blog Java le soir
Cours & Tutoriaux