Skip to main content

Welcome to a Grasp lesson

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

Lesson 48

Implémentation du pattern DAO

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons appris à parcourir un ResultSet et à mapper ses données vers des objets Java. Nous avons même esquissé une classe UserDAO pour y placer notre méthode findAll(). C'était un premier pas vers une meilleure organisation.

Aujourd'hui, nous allons formaliser cette organisation en mettant en œuvre l'un des design patterns les plus fondamentaux en développement backend Java : le Data Access Object (DAO). L'objectif est de structurer notre code d'accès aux données de manière professionnelle, en le séparant complètement de la logique métier de notre application. Cela rendra notre code plus propre, plus facile à maintenir et bien plus testable.

1. Pourquoi le pattern DAO ?

Dans notre exercice précédent, nous avons mis le code SQL dans une classe UserDAO. C'est mieux que de le laisser dans la méthode main, mais ce n'est pas encore idéal. Imaginez que votre application grandisse : vous aurez du code JDBC mélangé avec des validations, des calculs, etc. Que se passerait-il si vous deviez changer de base de données, passant de MySQL à PostgreSQL ? Vous devriez modifier du code à de nombreux endroits.

Le pattern DAO résout ce problème en créant une couche d'abstraction dédiée à la persistance des données.

Le modèle DAO - Créez votre application web avec Java EE

Cet article de Zeste de Savoir explique très bien le problème que le pattern DAO vient résoudre. Il met en lumière les inconvénients de mélanger la logique métier et la logique de stockage.

Lisez la première partie de l'article, jusqu'à la section "Isoler le stockage des données". Concentrez-vous sur les schémas qui illustrent la situation "avant" et "après" l'introduction de la couche DAO.

L'idée principale est simple : la logique métier de votre application (par exemple, "créer un nouvel utilisateur") ne devrait pas savoir comment cet utilisateur est sauvegardé. Elle délègue cette tâche à un objet spécialisé : le DAO.


2. L'architecture du pattern DAO

Le pattern DAO repose sur une séparation claire des responsabilités, généralement matérialisée par quatre composants :

  1. Le Bean (ou POJO) : C'est notre objet métier, une simple classe Java représentant une entité. Nous avons déjà notre classe Utilisateur.
  2. L'interface DAO : C'est le contrat. Elle définit les opérations de persistance possibles (par exemple, creer, trouver, lister) sans en donner le code. Le reste de l'application ne communiquera qu'avec cette interface.
  3. L'implémentation DAO : C'est la classe concrète qui implémente l'interface. C'est ici, et uniquement ici, que se trouve le code JDBC (les Connection, PreparedStatement, ResultSet, etc.).
  4. La Factory (Fabrique) DAO : C'est une classe utilitaire dont le rôle est de fournir des instances de l'implémentation du DAO. Cela permet de centraliser la création des objets DAO et de gérer la connexion à la base de données.

La vidéo ci-dessous résume très bien cette architecture.

Développez des sites web avec Java EE: Utiliser le modèle DAO

Cette vidéo de Cours informatique-G explique visuellement le rôle du DAO comme intermédiaire et l'importance de l'interface pour l'abstraction.

Regardez les deux premières sections de la vidéo : Introduction au DAO : pour comprendre le problème et la solution proposée. Architecture avec interface : pour visualiser comment l'interface permet de découpler l'application du système de stockage (MySQL, Oracle, fichiers, etc.).

Le schéma suivant, tiré de la documentation de Zeste de Savoir, illustre parfaitement cette structure.

Ce diagramme montre comment la couche métier interagit uniquement avec l'interface DAO. L'implémentation, qui contient le code SQL spécifique, est cachée. C'est le principe fondamental de l'encapsulation appliqué à la persistance des données.

3. Mise en œuvre étape par étape

Nous allons maintenant refactoriser notre code pour implémenter correctement le pattern DAO pour notre entité Utilisateur.

Étape 1 : Créer l'interface UtilisateurDao

L'interface est le contrat que l'implémentation devra respecter. Elle définit les opérations CRUD (Create, Read, Update, Delete) de base pour nos utilisateurs.

Le modèle DAO - Créez votre application web avec Java EE

Cette section de l'article de Zeste de Savoir montre exactement comment définir une interface pour un DAO.

Lisez cette courte section. Notez comment les méthodes sont définies et comment elles peuvent déclarer lever une DAOException (nous y reviendrons plus tard).

Créons notre interface UtilisateurDao.java. Pour l'instant, nous nous contenterons des méthodes de création et de lecture.

// src/main/java/com/example/dao/UtilisateurDao.java
package com.example.dao;

import com.example.model.Utilisateur;
import java.util.List;
import java.util.Optional;

public interface UtilisateurDao {
    void creer(Utilisateur utilisateur);
    Optional<Utilisateur> trouverParId(int id);
    List<Utilisateur> lister();
    // void mettreAJour(Utilisateur utilisateur);
    // void supprimer(int id);
}

Remarque : J'utilise ici Optional<Utilisateur> pour la méthode trouverParId. C'est une bonne pratique en Java moderne pour indiquer qu'un résultat peut être absent (l'utilisateur n'existe pas), évitant ainsi les retours null qui peuvent causer des NullPointerException.

Étape 2 : Créer l'implémentation UtilisateurDaoImpl

C'est le cœur de notre couche de persistance. Cette classe contiendra tout le code JDBC.

Le modèle DAO - Créez votre application web avec Java EE

Cette section est la plus importante et la plus dense. Elle détaille la création de la classe d'implémentation, UtilisateurDaoImpl. Elle introduit également des concepts très importants de code professionnel.

Lisez cette section en portant une attention particulière aux points suivants : Le constructeur : qui reçoit une instance de DAOFactory pour pouvoir obtenir une connexion. Les méthodes utilitaires : initialisationRequetePreparee() et fermeturesSilencieuses(). C'est une excellente pratique pour éviter la duplication de code. Le mapping : la méthode map() qui transforme une ligne de ResultSet en un objet Utilisateur. L'implémentation des méthodes trouver() et creer() qui utilisent toutes ces briques.

Inspirons-nous de cet article pour créer notre UtilisateurDaoImpl.java. Pour simplifier, nous passerons directement l'objet Connection dans le constructeur.

// src/main/java/com/example/dao/UtilisateurDaoImpl.java
package com.example.dao;

import com.example.model.Utilisateur;

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

public class UtilisateurDaoImpl implements UtilisateurDao {

    private Connection connection;

    public UtilisateurDaoImpl(Connection connection) {
        this.connection = connection;
    }

    @Override
    public void creer(Utilisateur utilisateur) {
        // Le try-with-resources s'assure que le PreparedStatement est bien fermé
        try (PreparedStatement ps = connection.prepareStatement(
                "INSERT INTO users (login, password) VALUES (?, ?)", 
                Statement.RETURN_GENERATED_KEYS)) {
            
            ps.setString(1, utilisateur.getLogin());
            ps.setString(2, utilisateur.getPassword());
            ps.executeUpdate();

            // Récupérer l'ID auto-généré
            try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
                if (generatedKeys.next()) {
                    utilisateur.setId(generatedKeys.getInt(1));
                }
            }

        } catch (SQLException e) {
            // Dans une vraie application, on lancerait une exception personnalisée
            e.printStackTrace();
        }
    }

    @Override
    public Optional<Utilisateur> trouverParId(int id) {
        // ... Implémentation à faire dans l'exercice
        return Optional.empty();
    }

    @Override
    public List<Utilisateur> lister() {
        List<Utilisateur> utilisateurs = new ArrayList<>();
        // Le try-with-resources s'assure que Statement et ResultSet sont bien fermés
        try (Statement stmt = connection.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT id, login, password FROM users")) {

            while (rs.next()) {
                utilisateurs.add(mapResultSetToUtilisateur(rs));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return utilisateurs;
    }

    // Méthode utilitaire pour le mapping
    private Utilisateur mapResultSetToUtilisateur(ResultSet rs) throws SQLException {
        int id = rs.getInt("id");
        String login = rs.getString("login");
        String password = rs.getString("password");
        return new Utilisateur(id, login, password);
    }
}

Étape 3 : Utiliser le DAO

Maintenant, notre code "client" (par exemple, la méthode main) devient beaucoup plus simple et lisible. Il ne manipule plus de SQL.

// Dans la méthode main
try (Connection connection = DriverManager.getConnection(url, user, password)) {
    
    // On instancie notre DAO avec la connexion
    UtilisateurDao utilisateurDao = new UtilisateurDaoImpl(connection);

    // 1. Lister tous les utilisateurs
    System.out.println("--- Liste des utilisateurs ---");
    List<Utilisateur> utilisateurs = utilisateurDao.lister();
    utilisateurs.forEach(System.out::println);

    // 2. Créer un nouvel utilisateur
    System.out.println("\n--- Création d'un nouvel utilisateur ---");
    Utilisateur nouvelUtilisateur = new Utilisateur(0, "charles", "pass123");
    utilisateurDao.creer(nouvelUtilisateur);
    System.out.println("Utilisateur créé : " + nouvelUtilisateur); // L'ID a été mis à jour

    // On reliste pour voir le nouvel ajout
    System.out.println("\n--- Nouvelle liste ---");
    utilisateurDao.lister().forEach(System.out::println);

} catch (SQLException e) {
    e.printStackTrace();
}

Vous voyez la différence ? Le code de la méthode main est clair : il exprime des intentions métier (lister, creer) et ne se préoccupe plus des détails techniques de la base de données.


4. Exercice : Compléter le DAO

Votre mission est de compléter notre UtilisateurDaoImpl et de le tester.

  1. Copiez le code : Reprenez la classe Utilisateur de la leçon précédente, et créez les fichiers UtilisateurDao.java et UtilisateurDaoImpl.java avec le contenu fourni ci-dessus. Placez-les dans des packages appropriés (par ex. com.example.model et com.example.dao).
  2. Implémentez trouverParId(int id) : Complétez le corps de cette méthode dans UtilisateurDaoImpl.
    • Elle doit utiliser un PreparedStatement avec une clause WHERE id = ?.
    • Comme un seul résultat est attendu, utilisez if (rs.next()) au lieu d'une boucle while.
    • Si un utilisateur est trouvé, utilisez la méthode mapResultSetToUtilisateur pour créer l'objet.
    • Retournez un Optional.of(utilisateur) s'il est trouvé, et Optional.empty() sinon.
  3. Testez votre méthode : Dans votre main, après avoir créé un utilisateur, utilisez sa nouvelle ID (qui est dans l'objet nouvelUtilisateur) pour le retrouver avec trouverParId(). Affichez le résultat.
Cliquez ici pour voir une solution possible pour `trouverParId`
@Override
public Optional<Utilisateur> trouverParId(int id) {
    Utilisateur utilisateur = null;
    String sql = "SELECT id, login, password FROM users WHERE id = ?";
    
    try (PreparedStatement ps = connection.prepareStatement(sql)) {
        ps.setInt(1, id);
        
        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) {
                utilisateur = mapResultSetToUtilisateur(rs);
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    
    return Optional.ofNullable(utilisateur);
}

Pour le test dans main :

// ... après avoir créé nouvelUtilisateur

System.out.println("\n--- Recherche de l'utilisateur créé ---");
Optional<Utilisateur> foundUser = utilisateurDao.trouverParId(nouvelUtilisateur.getId());
foundUser.ifPresentOrElse(
    u -> System.out.println("Utilisateur trouvé : " + u),
    () -> System.out.println("Utilisateur non trouvé.")
);

Conclusion

Félicitations ! Vous venez de mettre en œuvre le pattern DAO, une compétence absolument essentielle pour tout développeur Java backend. Vous avez créé une séparation nette entre la logique métier et l'accès aux données.

Les points clés à retenir sont :

  • Le pattern DAO encapsule la logique de persistance des données.
  • Il repose sur un quatuor : Bean, Interface DAO, Implémentation DAO, et souvent une Factory.
  • L'interface est le contrat, elle garantit le découplage. Le reste de l'application ne dépend que d'elle.
  • L'implémentation contient le code technique (JDBC), la rendant interchangeable.
  • Cette structure améliore grandement la lisibilité, la maintenabilité et la testabilité du code.

Dans nos prochaines leçons, nous aborderons la mise en œuvre des opérations UPDATE et DELETE, ainsi que la gestion des transactions, pour avoir un DAO entièrement fonctionnel. Ensuite, nous intégrerons cette couche DAO dans une véritable application web avec des Servlets.