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

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.
- Copiez le code : Reprenez la classe
Utilisateurde la leçon précédente, et créez les fichiersUtilisateurDao.javaetUtilisateurDaoImpl.javaavec le contenu fourni ci-dessus. Placez-les dans des packages appropriés (par ex.com.example.modeletcom.example.dao). - Implémentez
trouverParId(int id): Complétez le corps de cette méthode dansUtilisateurDaoImpl.- Elle doit utiliser un
PreparedStatementavec une clauseWHERE id = ?. - Comme un seul résultat est attendu, utilisez
if (rs.next())au lieu d'une bouclewhile. - Si un utilisateur est trouvé, utilisez la méthode
mapResultSetToUtilisateurpour créer l'objet. - Retournez un
Optional.of(utilisateur)s'il est trouvé, etOptional.empty()sinon.
- Elle doit utiliser un
- Testez votre méthode : Dans votre
main, après avoir créé un utilisateur, utilisez sa nouvelle ID (qui est dans l'objetnouvelUtilisateur) pour le retrouver avectrouverParId(). 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.