lundi 13 novembre 2017

Le Seigneur des DAO

Les objets d'accès aux données (Data Access Objects - DAO) servent de point d'interaction entre les objets métiers (ceux que vous gérez dans votre code) et la couche de persistence.

Chaque DAO est spécialisé sur un objet métier et agrège les opérations CrrrrrUD, à savoir la création (Create) modification (Update) suppression (Delete) + une ribambelle d'accès en lecture (Read) sous diverses formes : obtenir la liste des éléments, ou obtenir un élément par son ID, par son nom, par l'age du capitaine, etc.

Dans ce qui suit, j'utilise une couche DAO existante dont je veux altérer le comportement. Je vais utiliser un point d'entrée unique, une sorte de master classe Dao qui me donnera accès à tous les autres. Voyons comment.

Un des acteurs du Seigneur des Daho
- FIXME : pas la même typo ! -

Anatomie d'un DAO



Un DAO bien fait, c'est un DAO qui sépare son implémentation des méthodes qu'il expose. Dit autrement, c'est une interface et une classe. Les DAOs que j'ai utilisé ressemblaient à cela :
com.example.machins.dao.ITrucDao
com.example.machins.dao.IMucheDao
com.example.machins.dao.impl.TrucDaoImpl
com.example.machins.dao.impl.MucheDaoImpl
Je n'adhère pas particulièrement à la convention de nommage pour les interfaces consistant à préfixer le nom des interfaces par un I (ITruc), comme dit dans un article prédédent, mais faisons avec ce dont nous disposons ; au moins le nommage des classes est cohérent : elles se trouvent toutes dans le même package et terminent toutes par "Impl".

La définition d'un DAO ressemble à ceci :
package com.example.machins.dao;

import java.util.List;
import com.example.machins.model.Truc;

public interface ITrucDao {

    Truc getTrucById(Long id);

    List<Truc> getTrucs();

    long create(Truc truc);

    void update(Truc truc);

    void delete(Truc truc);

}
Dans ce modèle, suite à une création, la couche persistence retourne l'ID qui a servi à créer la ressource, à toutes fins utiles. Le détail de l'implémentation n'est pas intéressant en soit, mais n'importe quelle technologie d'Hibernate à JDBC fera l'affaire. Pour utiliser ces DAOs, il est commun de voir le code suivant (après avoir importé proprement les packages appropriés) :
ITrucDao trucDao = new TrucDaoImpl();
IMucheDao mucheDao = new MucheDaoImpl();

List<Truc> trucList = trucDao.getTrucs();
trucList.stream().forEach(truc -> {
    // utiliser chaque truc
});

Altérer les DAO



L'objectif est d'altérer l'accès aux DAO. Dans la vraie vie, je me suis retrouvé avec des DAOs existants, disponibles dans une archive, avec une tâche à réaliser en batch. Je voulais pouvoir exécuter mon code en mode "dry run", c'est à dire en désactivant chaque mise à jour, et accessoirement logger les opérations. Et accessoirement les 2 à la fois.

Plusieurs technologies s'offrent à moi :
  • utiliser des "mocks", c'est à dire un représentant de mon objet qui ne fait rien ; c'est une technique couramment utilisée pour tester
  • utiliser un container CDI (type Weld) pour injecter des implémentations spécifiques en fonction des besoins ; très commode également pour tester

Le problème du mock est qu'il masque tout, alors que je ne veux inhiber que les mises à jour, pas les lectures. Le problème du container est qu'il va falloir me battre pour trouver comment combiner 2 implémentations lorsque je veux à la fois l'option "dry run" et l'option "log DAO". Je ne suis pas un expert javax.inject

Je pourrais aussi agir au niveau de la couche persistance, vraisemblablement au moins trouver l'option-qui-va-bien pour logger, et plus difficilement un moyen pour inhiber les mises à jour. Mais là je perds mon indépendance avec mes DAOs.

Je décide de partir from scratch la fleur au fusil et développer ma propre solution. Il en résulte un pattern simple, immédiat, propre, et réutilisable.

Fournir les implémentations alternatives



Tout d'abord, je produit le code pour l'option "dry run", que je mets dans un paquet dédié. La consistance du nommage est importante pour la suite :
package com.example.machins.dao.impl.dry;

import java.util.List;
import com.example.machins.model.Truc;
import com.example.machins.dao.impl.TrucDaoImpl;
import static com.example.machins.dao.Dao.dummyId;

public class TrucDaoDryRun extends TrucDaoImpl {

    @Override
    public long create(Truc truc) {
        dummyId(); // TODO
    }

    @Override
    public void update(Truc truc) { }

    @Override
    public void delete(Truc truc) { }

}
Je laisse les méthodes getTrucById() et getTrucs() accéder aux données, et me contente de surcharger les implémentations pour les méthodes qui font des mises à jour. Comme il n'y a pas d'accès à la couche persistence, je suis obligé en création de fournir un ID factice. La classe Dao fournit la méthode dummyId() (qui se contente de retourner un ID aléatoire) et sera le point d'accès de tous les DAOs (détaillé ci-après).

Pour l'option "log DAO", chaque implémentation est une façade devant l'implémentation concrète demandée :
package com.example.machins.dao.impl.log;

import java.util.List;
import java.util.logging.Logger;
import com.example.machins.model.Truc;
import com.example.machins.dao.ITrucDao;
import com.example.machins.dao.impl.TrucDaoImpl;
import static com.example.machins.dao.Dao.$;

public class TrucDaoLog extends TrucDaoImpl {

    static Logger LOG = Logger.getLogger(ITrucDao.class.getName());

    static final ITrucDao DAO = $();

    @Override
    public long create(Truc truc) {
        LOG.info("Creating " + truc);
        return DAO.create(truc);
    }

    @Override
    public void update(Truc truc) {
        LOG.info("Updating " + truc);
        DAO.update(truc);
    }

    @Override
    public void delete(Truc truc) {
        LOG.info("Deleting " + truc);
        DAO.delete(truc);
    }

}
La classe Dao fournit la méthode $() qui consiste à fournir "l'implémentation appropriée de la classe appropriée", et qui sera détaillée ci-après.

Pour finir, la classe Dao contient une méthode d'accès pour chaque DAO :
    public ITrucDao getTrucDao() {
        // TODO
    }
ce qui en fait le point d'accès de tous les DAOs. Il ne faut plus utiliser de "new" sur le DAO, mais à la place utiliser cette méthode :
// ITrucDao trucDao = new TrucDaoImpl(); // don't use any longer !
// IMucheDao mucheDao = new MucheDaoImpl();

List<Truc> trucList = Dao.getDaoTruc().getTrucs();
trucList.stream().forEach(truc -> {
    // utiliser chaque truc
});

Un DAO pour les gouverner tous



Intéressons-nous maintenant à notre classe Dao. Tout d'abord, elle comporte 2 flags qui indiqueront quelles implémentations fournir :
public class Dao {

    static Logger LOG = Logger.getLogger(Dao.class.getName());

    /** When true, all writing operations are ignored. */
    public static boolean dryRun = false;

    /** When true, all writing operations are logged (even with dry run). */
    public static boolean logCrUD = false;

}
La première chose à faire avant toute chose sera de positionner ces flags, par exemple en lisant un fichier de propriété, les arguments de la ligne de commande, ou des variables d'environnement, peu importe.

Pour chaque DAO, il faut créer un singleton. Comme je ne sais pas ce que fait l'implémentation, je crée mes singletons sur demande. Je pourrais donc créer un champ et s'il est à null j'obtiens une instance ; et pour éviter les conflits (si j'utilisais ce code en dehors d'un batch) je synchronise la méthode. Personnellement, je n'aime pas, car chaque appel sera pénalisé par la directive de synchronisation. Et si vous étiez tenté de pré-tester la nullité de votre champ avant de synchroniser le bloc qui crée l'instance, vous n'obtiendrez pas l'effet escompté en raison du mécanisme de "out-of-order writes" interne à la JVM (voir à ce sujet un précédent article).

Le pattern le plus élégant (et fiable) du singleton en mode lazy est le suivant, et mis en oeuvre pour chaque DAO :
    // ======== every DAO follow the same pattern

    // true singleton pattern without explicit sync 
    // and without the flaw of "double-check locking"
    private static class TrucDao {
        private static final ITrucDao DAO = $(); // retrieve the right instance
    }
    public static ITrucDao getTrucDao(){
        return TrucDao.DAO;
    }
    // ======== DAO pattern end
Ce faisant, d'une part les instances de DAO ne sont produites que lorsqu'on les demande, et d'autre part on n'utilise plus de synchronisation explicite, mais on a recours à la synchronisation implicite que la JVM utilise lors du chargement des classes, qui n'aura donc lieu qu'une seule fois par DAO (c'est la claaaasse !).

Tous mes DAOs utiliseront le même pattern, et appellent tous la mystérieuse méthode $(), dont l'objet est de fournir la bonne implémentation en fonction des flags positionnés (notez que cette méthode est également appelée depuis les classes de Log) et en fonction de qui la demande. Pour obtenir la classe qui demande, j'ai recours au code utilitaire suivant, qui est une classe interne à la classe Dao :
    // allow to find the caller DAO
    private static class DaoFinder extends SecurityManager  {

        static Class<?> getDaoClass() {
            return $.$(); // return the caller class, 
                          // e.g. com.example.machins.dao.Dao$TrucDao
        }

        static final DaoFinder $ = new DaoFinder();

        @SuppressWarnings("rawtypes")
        Class<?> $() {
            Class[] classes = getClassContext();
            // call stack :
            // [class com.example.machins.dao.Dao$DaoFinder,
            //  class com.example.machins.dao.Dao$DaoFinder,
            //  class com.example.machins.dao.Dao,
            //  class com.example.machins.dao.Dao$TrucDao, <-- this one
            // ...]
            return classes[3];
        };

    }

Il ne reste qu'à implémenter la méthode $() :
    /**
     * Get the right DAO implementation according to the caller.
     * 
     * @return The relevant implementation with the required style (dry run, log, impl)
     */
    public static <T> T $() {
        // return the right implementation :
        // -get the name of the caller class
        // -return the "active" implementation OR the "dry run" implementation OR the "log" implementation
        String daoName = DaoFinder.getDaoClass().getSimpleName(); // e.g. TrucDao or TrucDaoLog
        String symbol = "";
        if (daoName.endsWith("Log") || ! logCrUD) {
            if (daoName.endsWith("Log")) {
                daoName = daoName.substring(0, daoName.length() - 3);
            }
            if (dryRun) {
                symbol = "🗑";
                daoName = "com.example.machins.dao.impl.dry." + daoName + "DryRun";
            } else {
                symbol = "📦";
                daoName = "com.example.machins.dao.impl." + daoName + "Impl";
            }
        } else if (logCrUD) {
            symbol = "🔍";
            daoName = "com.example.machins.dao.impl.log." + daoName + "Log";
        }
        LOG.info("🔧 Configuring DAO " + symbol + " " + daoName);
        try {
            @SuppressWarnings("unchecked")
            T dao = (T) Class.forName(daoName).newInstance();
            return dao;
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
A l'exécution, j'obtiens ceci dans les logs :
  • En mode "dry run" et "log DAO" :
    INFOS: 🔧 Configuring DAO 🔍 com.example.machins.dao.impl.log.TrucDaoLog
    nov. 13, 2017 4:01:39 PM com.example.machins.dao.Dao $
    INFOS: 🔧 Configuring DAO 🗑 com.example.machins.dao.impl.dry.TrucDaoDryRun
    nov. 13, 2017 4:01:39 PM com.example.machins.dao.Dao $
    
    On voit bien que le Log DAO demande aussi une instance, mais il ne choisit pas laquelle.
  • En mode "dry run" :
    INFOS: 🔧 Configuring DAO 🗑 com.example.machins.dao.impl.dry.TrucDaoDryRun
    nov. 13, 2017 4:01:39 PM com.example.machins.dao.Dao $
    
  • En mode "log DAO" :
    INFOS: 🔧 Configuring DAO 🔍 com.example.machins.dao.impl.log.TrucDaoLog
    nov. 13, 2017 4:01:39 PM com.example.machins.dao.Dao $
    INFOS: 🔧 Configuring DAO 📦 com.example.machins.dao.impl.TrucDaoImpl
    nov. 13, 2017 4:01:39 PM com.example.machins.dao.Dao $
    
  • En mode normal :
    INFOS: 🔧 Configuring DAO 📦 com.example.machins.dao.impl.TrucDaoImpl
    nov. 13, 2017 4:01:39 PM com.example.machins.dao.Dao $
    
Je pense qu'avec un peu d'effort supplémentaire, on pourrait générer les implémentations "dry run" et "log DAO" à la volée ou à la compilation avec quelques annotations, étant donné qu'aucune de ces implémentations n'est nommée explicitement dans le code.

Exercice : utilisation de CDI



Si vous êtes étudiant, en guise de devoir à la maison, vous pouvez utiliser l'injection de dépendance afin d'obtenir le même résultat. Merci de poster votre solution alternative (ou un lien vers) dans les commentaires.






Semantic Mismatch
ERR-19 : IllegalArgumentException
POLITIQUE is not applicable to AUTRUCHE
-- In "La politique de l'autruche"
-- See log for details



Aucun commentaire:

Enregistrer un commentaire