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.
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 :
La définition d'un DAO ressemble à ceci :
com.example.machins.dao.ITrucDao com.example.machins.dao.IMucheDao
com.example.machins.dao.impl.TrucDaoImpl com.example.machins.dao.impl.MucheDaoImplJe 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()); /** WhenLa 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.true
, all writing operations are ignored. */ public static boolean dryRun = false; /** Whentrue
, all writing operations are logged (even with dry run). */ public static boolean logCrUD = false; }
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 endCe 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 $
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