Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 47

Navigation et extraction de données d'un ResultSet

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons appris à exécuter des requêtes de manière sécurisée avec PreparedStatement, nous protégeant ainsi contre les injections SQL. Nous avons vu comment envoyer des données à la base de données. Il est maintenant temps de maîtriser la réception et la manipulation des données qu'elle nous renvoie.

Aujourd'hui, nous allons nous concentrer sur l'objet qui encapsule les résultats d'une requête SELECT : le ResultSet. L'objectif est de devenir capable de parcourir n'importe quel ensemble de résultats, d'en extraire les données de manière fiable, et de les transformer en objets Java utilisables dans notre application. C'est une compétence essentielle pour construire la moindre fonctionnalité qui lit des informations depuis une base de données.

1. Qu'est-ce qu'un ResultSet ?

Lorsqu'on exécute une requête SELECT avec executeQuery(), le pilote JDBC nous retourne un objet de type java.sql.ResultSet. Il est préférable de se le représenter non pas comme une collection (telle qu'une ArrayList), mais plutôt comme une vue tabulaire des données, équipée d'un curseur.

Ce curseur pointe initialement avant la première ligne de résultats. Pour accéder aux données, nous devons déplacer ce curseur ligne par ligne.

Comment récupérer des données avec ResultSet en Java?

Cet article explique très clairement le concept du curseur et le mécanisme d'itération de base.

Lisez la première partie de l'article, "Lecture d’un ResultSet". Concentrez-vous sur le code d'exemple et le schéma qui illustre le mouvement du curseur à chaque appel de rs.next().

L'image ci-dessous résume visuellement le processus : une requête SQL est exécutée, ce qui produit un ResultSet, que nous allons ensuite parcourir pour en extraire les informations.

Cette image illustre le flux : une requête `SELECT` sur la table `users` retourne un objet `ResultSet`. On utilise ensuite une boucle et les méthodes `getXXX()` pour extraire les données de chaque ligne.

2. Parcourir les résultats et extraire les données

Le parcours d'un ResultSet se fait presque toujours avec une boucle while. La méthode clé est rs.next(), qui accomplit deux actions :

  1. Elle avance le curseur à la ligne suivante.
  2. Elle retourne un boolean : true si le curseur se trouve sur une ligne valide (il y a donc des données à lire), et false s'il a dépassé la dernière ligne.

Une fois que le curseur est positionné sur une ligne, nous pouvons extraire les données de chaque colonne à l'aide des méthodes getXXX(), où XXX est le type de données Java que l'on souhaite obtenir (String, Int, Double, Date, etc.).

Il existe deux manières d'identifier la colonne à lire :

  1. Par son nom (label) : rs.getString("login")
    • Avantages : Très lisible et robuste. Si vous changez l'ordre des colonnes dans votre SELECT, le code continue de fonctionner. C'est la méthode recommandée.
  2. Par son index : rs.getInt(1)
    • Avantages : Potentiellement un peu plus performant (la différence est souvent négligeable).
    • Inconvénient majeur : L'indexation des colonnes en JDBC commence à 1, et non à 0 comme pour les tableaux ou les listes en Java. C'est une source d'erreurs très fréquente.

La vidéo suivante montre une démonstration complète de ce processus.

TUTO Java JDBC : Prise en main de l'API JDBC

Cette partie de la vidéo de Dominique Liard, que nous avons déjà consultée, met en pratique l'exécution d'une requête SELECT et la lecture du ResultSet retourné.

Regardez attentivement le segment de l'exécution de la requête SELECT. Portez une attention particulière aux points suivants : La structure de la boucle while (rs.next()). L'utilisation des méthodes getInt() et getString() pour récupérer les données. L'exemple qui mélange l'accès par index (getInt(1)) et par nom de colonne (getString("password")). La mise en garde de l'auteur concernant l'indexation qui commence à 1 (le piège de l'indice 1). L'utilisation du try-with-resources pour gérer à la fois le Statement et le ResultSet, garantissant leur fermeture automatique.

Voici un résumé des méthodes getXXX() les plus courantes :

Méthode Type de retour Java
getBoolean() boolean
getDouble() double
getInt() int
getLong() long
getString() String
getDate() java.sql.Date
getTimestamp() java.sql.Timestamp
getObject() Object

3. Cas particulier : un seul résultat attendu

Parfois, une requête est conçue pour ne retourner qu'une seule ligne (par exemple, SELECT * FROM users WHERE id = ?) ou une seule valeur (ex: SELECT COUNT(*) FROM users). Dans ce cas, une boucle while est inappropriée. On utilise plutôt une instruction if.

public int countUsers(Connection connection) throws SQLException {
    String sql = "SELECT COUNT(*) FROM users";
    int count = 0;
    
    try (Statement stmt = connection.createStatement();
         ResultSet rs = stmt.executeQuery(sql)) {
        
        // On vérifie s'il y a bien une ligne de résultat
        if (rs.next()) {
            // On récupère la valeur de la première (et unique) colonne
            count = rs.getInt(1); 
        }
    }
    return count;
}

Point crucial : Il est obligatoire d'appeler rs.next() et de s'assurer qu'elle retourne true avant de tenter d'appeler une méthode getXXX(). Si vous essayez de lire des données alors que le curseur n'est pas sur une ligne valide (soit avant la première ligne, soit après la dernière), votre programme lèvera une SQLException.

Comment récupérer des données avec ResultSet en Java?

Cette section de l'article précédent se concentre spécifiquement sur ce cas d'usage.

Lisez la section "Récupération d’une Seule Ligne". Elle illustre bien l'utilisation de if (rs.next()) et explique les SQLException qui se produisent si vous essayez d'accéder aux données au mauvais moment.


4. Mapper un ResultSet à des Objets Java

En pratique, on ne se contente que rarement d'afficher les données dans la console. Le but est de les transformer en objets de notre domaine métier (des "POJOs" - Plain Old Java Objects) pour les utiliser dans le reste de l'application.

Ce processus, appelé mapping, consiste à lire chaque ligne du ResultSet et à utiliser ses valeurs pour instancier et peupler un objet Java.

Cette image illustre parfaitement le concept : les données de la table 'order' sont récupérées dans un ResultSet. Ensuite, les valeurs de chaque ligne (comme 'id', 'name', 'date') sont extraites pour créer et remplir les champs d'un objet Java de la classe 'Order'.

Voici à quoi ressemble le code pour mapper la table users à une liste d'objets User.

Supposons que nous ayons cette classe User :

public class User {
    private int id;
    private String login;
    private String password;

    // Constructeurs, getters, setters et toString()...
}

La méthode de récupération et de mapping ressemblerait à ceci :

public List<User> getAllUsers(Connection connection) throws SQLException {
    String sql = "SELECT id, login, password FROM users";
    List<User> userList = new ArrayList<>();

    try (Statement stmt = connection.createStatement();
         ResultSet rs = stmt.executeQuery(sql)) {
        
        while (rs.next()) {
            // 1. Extraire les données de la ligne courante
            int id = rs.getInt("id");
            String login = rs.getString("login");
            String password = rs.getString("password");
            
            // 2. Créer un nouvel objet User avec ces données
            User user = new User(id, login, password);
            
            // 3. Ajouter l'objet à la liste
            userList.add(user);
        }
    }
    
    return userList;
}

5. Exercice Pratique : Lister tous les utilisateurs

Mettons tout cela en pratique. Vous allez créer une méthode qui récupère tous les utilisateurs de la base de données et les retourne sous forme d'une liste d'objets.

  1. Préparez la classe User :
    Créez un fichier User.java dans votre projet et ajoutez-y une classe User avec les attributs id (int), login (String), et password (String). N'oubliez pas de générer un constructeur, les getters, les setters, et une méthode toString() pour faciliter l'affichage.

  2. Créez une classe UserDAO :
    Nous anticipons un peu sur la prochaine leçon en introduisant le pattern DAO (Data Access Object). Créez une classe UserDAO.java. Son rôle sera d'encapsuler toute la logique d'accès aux données pour les utilisateurs.

  3. Implémentez la méthode findAll() :
    Dans votre classe UserDAO, créez la méthode public List<User> findAll(Connection connection) throws SQLException.

    • Votre tâche est d'implémenter cette méthode en suivant le modèle vu dans la section 4.
    • Elle doit exécuter une requête SELECT * FROM users.
    • Elle doit itérer sur le ResultSet, créer un objet User pour chaque ligne, et les ajouter à une ArrayList.
    • Finalement, elle doit retourner cette liste.
  4. Testez dans le main :
    Dans votre méthode main, après avoir établi la connexion, instanciez votre UserDAO, appelez la méthode findAll(), et parcourez la liste retournée pour afficher chaque utilisateur.

    // Dans votre méthode main, à l'intérieur du try-with-resources de la connexion
    try (Connection connection = DriverManager.getConnection(url, user, password)) {
        System.out.println("Connexion établie avec succès !");
    
        UserDAO userDAO = new UserDAO();
        List<User> users = userDAO.findAll(connection);
    
        System.out.println("Liste des utilisateurs :");
        for (User user : users) {
            System.out.println(user); // Fait appel à la méthode toString() de User
        }
    
    } catch (SQLException e) {
        // ...
    }
    
Cliquez ici pour voir une solution possible

User.java

public class User {
    private int id;
    private String login;
    private String password;

    public User(int id, String login, String password) {
        this.id = id;
        this.login = login;
        this.password = password;
    }

    // Getters et Setters...
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    @Override
    public String toString() {
        return "User{" +
               "id=" + id +
               ", login='" + login + '\'' +
               ", password='" + password + '\'' +
               '}';
    }
}

UserDAO.java

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class UserDAO {

    public List<User> findAll(Connection connection) throws SQLException {
        String sql = "SELECT id, login, password FROM users";
        List<User> userList = new ArrayList<>();

        try (Statement stmt = connection.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            while (rs.next()) {
                int id = rs.getInt("id");
                String login = rs.getString("login");
                String password = rs.getString("password");

                User user = new User(id, login, password);
                userList.add(user);
            }
        }
        return userList;
    }
}

Conclusion

Félicitations ! Vous savez maintenant comment interroger une base de données et, plus important encore, comment traiter et utiliser les données qu'elle vous retourne.

Les points clés de cette leçon sont :

  • Le ResultSet est un objet avec un curseur qui pointe initialement avant la première ligne.
  • La boucle while (rs.next()) est le standard pour parcourir plusieurs résultats, tandis que if (rs.next()) est utilisé pour un résultat unique.
  • On accède aux données avec les méthodes getXXX(), de préférence en utilisant le nom de la colonne pour la lisibilité et la robustesse.
  • N'oubliez jamais que l'indexation des colonnes commence à 1.
  • Le but final est souvent de mapper les lignes du ResultSet à des objets Java pour les utiliser dans la logique de votre application.
  • ResultSet est une ressource qui doit être fermée, idéalement via un bloc try-with-resources.

Nous avons établi une connexion, exécuté des requêtes sécurisées et lu les résultats. Cependant, notre code d'accès aux données est encore directement dans la classe main ou dans une classe peu structurée. Dans la prochaine leçon, nous allons formaliser l'organisation de ce code en implémentant proprement le pattern DAO (Data Access Object).