Service & DAO

GestCV, comme toute application, est constituée de nombreux traitements comme la création d'un collaborateur, la mise à jour d'un collaborateur.... Il est important de découper en couche une Application. Il existe différents patterns qui permettent de modéliser ces processus métier. GestCV sera découpée en deux couches :

  • couche Service : chacune des méthodes de cette couche représentera un processus métier. L'implémentation de ces méthodes appeleront les méthodes de la couche DAO.
  • couche DAO (Data Access Object): chacune des méthodes accéderont à la base de données, pour mettre à jour ou récupérer des enregistrements de la base de données.

Voici le schéma représentant le pattern Service/DAO

Ce découpage en deux couches peut s'expliquer à travers un exemple concret : La création d'un collaborateur. Un collaborateur se distingue par son login (le login est unique).

La création d'un collaborateur nécéssite :

  • la recherche de collaborateur par login (saisi) dans la base.
  • SI un collaborateur existe dejà avec le login (saisi), retourne une erreur.
  • SINON création du collaborateur dans la base.

Ce processus métier sera confiné dans la méthode createCollaborateur de la classe ServiceCollaborateur. Cette méthode fera appel aux méthodes findCollaborateurByLogin et createCollaborateur de la classe CollaborateurDAO qui permet l'accés à la base de données.

Voici un exemple du service ServiceCollaborateur :

  public class ServiceCollaborateur {
    
    public void createCollaborateur(Collaborateur collaborateur) throws Exception {
      String login = collaborateur.getLogin();
      if (collaborateurDAO.findCollaborateurByLogin(login) != null) {
        // le collaborateur existe, retourne une erreur
        throw new Exception("Le collaborateur de login " + login + "existe déjà.");
      }
      // Creation du collaborateur
      collaborateurDAO.createCollaborateur(collaborateur);
    }
  }

Voici un exemple de la DAO CollaborateurDAO :

  public class CollaborateurDAO {
  
    public Collaborateur findCollaborateurByLogin(String login) {
      // SELECT * FROM T_COLLABORATEUR WHERE COL_LOGIN_C = ? ...
      return ....;
    }
    
    public void createCollaborateur(Collaborateur collaborateur) {
      // INSERT INTO T_COLLABORATEUR ...
    }
  }

DAO & Criteria

Malgré tout, le Design Pattern DAO présente un inconvénient majeur. La tâche qui consiste à développer les DAO est relativement fastidieuse. En effet, les requêtes exécutées sont liées à la nature même des méthodes, autrement dit à chaque nouveau critère de recherche ajouté, la classe DAO doit implémenter une nouvelle méthode findByMyNewCriteria. Par exemple pour la DAO CollaborateurDAO, si l'on souhaite effectuer une recherche par nom, puis une autre recherche par login du collaborateur, il faudra implémenter deux Finder, findCollaborateurByName et findCollaborateurByLogin. Si l'on souhaite combiner la recherche par nom et login, il faudra implémenter un nouveau finder findCollaborateurByNameAndLogin.

Pour éviter d'implémenter un nouveau finder par nouveau critère ou par nouvelle combinaison de critères, gestCV implémentera un seul finder de type findByCriteria. Cette méthode attendra un objet de type Criteria, qui sera un POJO (Plain Old Java Object). Cet objet Java sera constitué de getter/setter qui correspondront aux différents critères de recherche de la DAO. L'interêt de ce mécanisme est de développer d'une part une seule méthode findByCriteria, mais aussi de pouvoir combiner plusieurs critères de recherche. Ainsi la DAO, s'enrichissera au cours du projet, à chaque ajout d'un nouveau critère de recherche.

Voici un exemple d'implémentation de la classe CollaborateurDAO, qui permet de rechercher un collaborateur par nom et/ou login :

  public class CollaborateurDAO {
  
    public Collaborateur findCollaborateurByCriteria(CollaborateurCriteria criteria) {
      // SELECT * FROM T_COLLABORATEUR WHERE ...
      if (criteria.getLogin() != null)
        // AND COL_LOGIN_C = ? ...
      if (criteria.getName() != null)
        // AND COL_NAME_C = ? ...        
      return ....;
    }
  }

Le service construira les critères de recherche adéquates selon les processus métier dont l'application a besoin :

  public class ServiceCollaborateur {
    
    public Collaborateur findCollaborateurByLogin(String login) {
      CollaborateurCriteria criteria = new CollaborateurCriteria();
      criteria.setLogin(login);
      return collaborateurDAO.findCollaborateurByCriteria(criteria);
    }
  }

DTO et Bean

Les objets manipulés par les Services sont appelés DTO (Data Transfert Object). L'objet Collaborateur est un objet de type DTO.

Il existe de nombreuses appélations de ces objets dans le monde J2EE, comme les DTO (Data Transfer Object), les VO (Value Object)...

Les services communiquent avec les DAO par des objets de type Bean. Une question vient alors à l'esprit : pourquoi utiliser des Bean dans les DAO alors qu'une DTO pourrait peut-être suffir ?

Dans une architecture EJB traditionnelle, les DTOs ont deux buts :

  • premièrement, ils contournent le problème des "entity bean" qui ne sont pas sérialisables.
  • deuxièmement, ils définissent implicitement une phase d'assemblage où toutes les données utilisées par la vue sont rapatriées et organisées dans les DTOs avant de retourner sous le contrôle de la couche de présentation.

Les DAO étant implémentées en Hibernate, le premier point est obsolète, car Hibernate permet d'utiliser n'importe quel type d'objet (POJO) que l'on peut rendre alors sérialisable.

Le deuxième point permet d'expliquer la raison pour laquelle il est nécéssaire de distinguer Bean et DTO. Prenons le cas d'un utilisateur qui peut être associé à une liste de rôles. Nous avons donc une table T_USER qui contient les informations de l'utilisateur, une table T_ROLE qui contient les rôles de l'application et une table T_USERROLE qui est une table d'association entre T_USER et T_ROLE.

Imaginons que dans une page de l'application, les informations de l'utilisateur doivent être affichées, et dans une autre page, les informations de l'utilisateur ET de ses rôles associés doivent être affichées.

La classe UserDTO ressemblerait à :

  public class UserDTO {
    
    private String name;
    ....
    private Collection roles;
    
    
    public void setName(String name) {
      this.name = name;
    }    
    public String getName() {
      return name;
    }
    
    public Collection getRoles() {
      return roles;
    }
    
  }

Les classes Bean sont mappées sur chacune des tables. La classe UserBean, qui permet de charger un utilisateur, ressemblerait à

  public class UserBean {
    
    private Integer id;
    private String name;
    ....
    
    public void setName(String name) {
      this.name = name;
    }    
    public String getName() {
      return name;
    }
    
    
  }

la classe UserRoleBean ressemblerait à :

  public class UserRoleBean {
    
    private Integer userId;
    private Colection roles; //Collection of RoleBean

    public void setUserId(Integer userId) {
      this.roleId = roleId;
    }    
    public Integer getUserId() {
      return userId;
    }
    
    public Collection roles() {
      return roles;
    }
    
    
  }

la classe RoleBean ressemblerait à :

  public class RoleBean {
    
    private String name;

    public void setName(String name) {
      this.name = name;
    }    
    public String getName() {
      return name;
    }
    
    
  }

Dans le premier cas, on récupère une instance UserBean, puis on popule les informations de celle-ci dans une instance de UserDTO. Dans le deuxième cas, on rècupère une instance de UserBean, puis une instance de UserRolebean qui contient la liste des RoleBean associée à l'utilisateur. Ces deux instances sont utilisées ensuite pour populer une instance de UserDTO avec ses données.

Cette exemple démontre la phase d'assemblage nécessaire pour populer la DTO selon un processus métier.

Cependant cet exemple n'est pas assez pertinent. En effet Hibernate donne la possibilité de charger des données à la demande, mécanisme appelé mode lazy. Imaginons que l'on décide de mapper l'objet UserDTO dans un mapping Hibernate. Il sera possible (en mappant la méthode roles en tant que lazy) de charger la liste des rôles de l'utilisateur dès que la méthode getRoles est appelée (plus précisemment d'itérer sur la collection retourné par getRoles ou d'appeler la méthode size() de getRoles). Ce chargement à la demande nécessite obligatoirement une session Hibernate ouverte (une connexion à la base ouverte). C'est ce point qui peut poser problème !!!

En effet, la session Hibernate étant fermé après l'appel d'un service, si une JSP tente d'itérer sur la liste des rôles de l'instance UserDTO, une erreur de connexion à la base sera déclenché. De plus, l'instance UserDTO étant persistante, si une action Struts tente de modifier une donnée de UserDTO (par l'appel d'un getter de cette instance; eg : userDTO.setName("myNewName")), cette modification engendrera une tentative d'update dans la base, ce qui provoquera une erreur SQL, car la connection n'est plus ouverte.

Une solution a ce problème serait de laisser la session Hibernate ouverte, jusquà la fin de l'execution de la JSP. C'est ce qu'on appelle le pattern Open In View. Ce pattern engendre un couplage très fort sur la facon d'utiliser les services. En effet, si une autre application WEB souhaite utiliser les services implémentés, elle devra obligatoirement gérer la session Hibernate correctement.

Une autre solution à ce problème serait de détacher l'instance UserDTO de la session Hibernate, autrement dit d'indiquer à la session factory d'Hibernate de ne plus surveiller cette instance. Cette solution permet de résoudre tous ces problèmes mais la DTO dépend des mappings Hibernate, ce qui engendre un fort couplage avec le modèle de données. Il peut arriver que le mapping Hibernate ne couvre pas tous les cas d'utilisation métier, ce qui obligerait à utiliser plusieurs DTO dans les actions.

DAO & Connexion

Jusqu'ici, la gestion de la connexion à la base de données n'a pas encore été abordée. Les DAO qui ont pour but de gérer les accès à la base de données, ont besoin de récupérer un objet Connection. Cet objet Connection permet ensuite :

  • d'éxecuter des requêtes de récupération de données (SELECT * FROM ...).
  • d'éxecuter des requêtes d'insertion de données (INSERT INTO...).
  • d'éxecuter des requêtes de modification de données (UPDATE...).
  • d'éxecuter des requêtes de suppression de données (DELETE...).

Cet objet Connection est capable de gérer des transactions, autrement dit d'éxecuter une succession de mises à jour (INSERT, UPDATE, DELETE,...) puis de sauvegarder en base cet ensemble de mises à jour en effectuant un commit ou de l'annuler en effectuant un rollback.

Face à ces fonctionnalités, plusieurs questions apparaîssent :

  • A quel endroit doit-on ouvrir une Connection ?
  • A quel endroit doit-on fermer une Connection ?
  • A quel endroit doit-on gérer les Transactions (commit et rollback) ?

Le pattern DAO préconise de gérer les Connections (ouverture/fermeture/transactions) au sein de chacune des méthodes de la classe DAO. Pour la DAO CollaborateurDAO, la recherche d'un collaborateur par critères ressemblerait à :

  
    public Collaborateur findCollaborateurByCriteria(CollaborateurByCriteria criteria) {
      Connection connection = null;
      try {
        // Ouverture de la connection
        connection = MyPoolConnection.getConnection();
        ...
        // Execution de la requête en utilisant la connexion ouverte
        // SELECT * FROM T_COLLABORATEUR WHERE ...
        if (criteria.getLogin() != null)
          // AND COL_LOGIN_C = ? ...
        if (criteria.getName() != null)
          // AND COL_NAME_C = ? ...   
      }
      catch(SQLException e) {
  
      }
      finally {
        // Fermeture de la connection
        try { 
            if (connection != null) connection.close(); 
        }
        catch(SQLException e) {}
      }
      return ....;
    }

La creation d'un collaborateur donnerait :

  
    public void createCollaborateur(Collaborateur collaborateur) {
      Connection connection = null;
      try {
        // Ouverture de la connection 
        connection = MyPoolConnection.getConnection();
        ...
        // Execution de la requête en utilisant la connexion ouverte
        // INSERT INTO T_COLLABORATEUR  ...
        // Commit de la connection
        connection.commit(); 
      }
      catch(SQLException e) {
        // Erreur => Rollback de la connection
        connection.rollback(); 
      }
      finally {
        // Fermeture de la connection
        try { 
            if (connection != null) {
              // Fermeture de la connection
              connection.close(); 
            }
        }
        catch(SQLException e) {}
      }
    }

Cette implémentation de la DAO soulève plusieurs problématiques :

  • Lors de la récupération de données, il y a ouverture/fermeture d'une connection à la base à chaque appel d'une méthode d'une DAO. Si l'on veut dans un processus charger un collaborateur puis charger sa liste de compétences, deux ouvertures et fermetures de connexion seront nécessaires alors qu'une seule suffirait.
  • La méthode createCollaborateur constitue une seule et unique Transaction. Il est impossible avec cette implémentation de gérer une transaction avec plusieurs appels de méthode DAO. Par exemple, la méthode createCollaborateur ne peut plus être réutilisée si on veut dans un autre processus, gérer une transaction qui créérait un collaborateur, puis lui affecterait une liste de compétences. Cette nouvelle transaction devra constituer une nouvelle méthode createCollaborateurWithCompetences.
  • Le code, qui permet l'ouverture/fermeture de la connection et la gestion de la transaction (commit/rollback), est redondant. Il est très facile au cours du développement d'oublier par exemple la ligne de code connection.close() qui deviendra source de problèmes.

Une solution à ces problèmes serait de déporter la gestion de la connection au niveau du Service et de passer la connection à la DAO. Ceci va à l'encontre du pattern Service qui doit faire abstraction de l'implémentation de la DAO. La DAO peut être implémentée en JDBC (ce qui implique de gérer un objet du type java.sql.Connection) ou en Hibernate (ce qui implique alors de gérer un objet du type org.hibernate.Session). Enfin cette solution ne permettra pas de résoudre le problème de redondance de code. Après avoir soulevé tous ses problèmes, la section Approche de l'AOP tentera d'expliquer comment Spring permet de régler tous ces problèmes.

DAO & Implementation

La couche Service fait appel à la couche DAO. Plus précisement une méthode d'une classe Service fera appel à des méthode d' instances de classes DAOs. Les DAO qui sont la couche qui communique avec la base de données peuvent être implementés de différentes manières, comme JDBC (SQL pur), Hibernate, ... GestCV implémentera les DAO en Hibernate.

A partir de là, on peut se poser la question suivante : qui et comment doit instancier les DAOs ? Généralement, les DAOs sont instancié à l'aide d'une classe Factory, DAOFactory qui a pour but de retourner une instance d'une classe en fonction d'un type demandé.

Ce pattern engendre les faits suivants:

  • le code dépend d'un appel à la classe DAOFactory.
  • les implémentations des différents DAO sont déclarées en dur dans la classe DAOFactory.

Pour des projets conséquents, ce pattern peut être pénalisant. En effet, il sera impossible de tester l'appel d'une méthode d'un service tant que les DAO (utilisé dans la méthode du service) ne seront pas développées.

Une solution a ce problème serait de développer une classe DAOTest qui simulerait la DAO final, mais ceci nécéssite la modification du code de la classe DAOFactoy (retour d'une instance DAOTest dans la factory), classes communes a tous développeurs du projet.

Après avoir soulevé tous ses problèmes, la section Approche de l'IoC tentera d'expliquer comment Spring permet de régler tous ces problèmes.