Aujourd'hui, je vous présente une architecture permettant d'obtenir des modèles Wicket totalement génériques s'appuyant de manière transparente sur des DAO Hibernate.
Cette intégration "back to front", s'appuyant sur les types génériques Java, couvre 80% des cas d'utilisation standard des modèles Wicket et permet de simplifier le code des pages.
Analyse du besoin
Dans la plupart des cas, nos modèles Wicket remplissent l'une des 3 fonctions suivantes :
- Récupérer une entité métier par son ID
- Récupérer une liste d'entités métiers, optionnellement triée (pour affichage simple)
- Récupérer une liste d'entités métiers, optionnellement triée et/ou paginée (pour les DataTable)
Prenons un exemple :
private class EmployeeModel extends LoadableDetachableModel<Employee> { @Autowired private EmployeeDao employeeDao; private final Long id; public EmployeeModel(Long id) { this.id = id; } @Override protected Employee load() { return employeeDao.getById(id); } }
Ce type de modèle très classique se rencontre dans quasiment toutes les pages Wicket. Pourtant, il est majoritairement composé de code boiler-plate, qui pourrait facilement être généré ou automatisé : le stockage de l'identifiant de l'entité, l'appel à la méthode getById(), l'enrobage dans un LoadableDetachableModel... En réalité, le seul élément réellement spécifique est le type de l'entité métier récupérée - même le DAO à appeler pourrait en être déduit !
Au passage, les puristes objecteront que la couche de présentation ne devrait pas accéder directement à la couche DAO ; mais quelle plus-value la couche de service apporterait-elle dans ce cas précis ? Aucune à mon sens. Eliminons donc les étapes superflues.
Architecture générique
La couche Modèle
L'unification et l'automatisation des couches est rendue possible par l'emploi des types génériques Java.
En particulier, tout repose sur le fait que les éléments de la couche Métier partagent un ancêtre commun que nous nommerons GenericEntity :
@MappedSuperclass public abstract class GenericEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; @Version private Long version; @Basic(optional = true) private boolean deleted = false; }
Cette classe de base fournit un ensemble de services comme la gestion de l'identifiant Hibernate (@Id) et du locking optimiste (@Version). De plus, comme il est rare que des données soient réellement supprimées en base, un simple flag nous permettra d'indiquer leur état.
Voyons maintenant une simple entité, par exemple Person :
@Entity public class Person extends GenericEntity { private String firstName; private String lastName; private String email; }
La couche DAO
Maintenant que nos entités possèdent toutes un ancêtre commun, il est facile de réaliser une couche DAO générique.
Dans notre architecture, un GenericEntityDao<T> (avec T extends GenericEntity) fournit une implémentation des besoins les plus courants (récupération d'une entité par son ID ou d'une liste d'entités, etc.). Evidemment, il sera ensuite possible de le sous-classer pour répondre à des besoins plus particuliers.
Petit détail technique : les limitations actuelles des Génériques ne permettent pas de déterminer le type exact du paramètre T au runtime, ni d'instancier un élément de type "T".
Nous emploierons donc la solution de contournement habituelle, qui consiste à fournir explicitement la classe de l'entité cible dans le constructeur.
public interface GenericEntityDao<T extends GenericEntity> { T save(T entity); T merge(T entity); void delete(T entity); T getById(Long id); List<T> getAll(); List<T> getAll(String propertyName, SortOrder order); List<T> getAllWithPagination(int start, int count); List<T> getAllWithPagination(int start, int count, String sortPropertyName, boolean sortAscendingOrder); int count(); Class<T> getEntityClass(); }
Voici l'implémentation Hibernate correspondante :
@Repository @Transactional public abstract class HibernateGenericEntityDao<T extends GenericEntity> implements GenericEntityDao<T> { @Autowired(required = true) private SessionFactory sessionFactory; private Class<T> entityClass; public HibernateGenericEntityDao(Class<T> entityClass) { this.entityClass = entityClass; } public T save(T entity) { getSession().saveOrUpdate(entity); return entity; } public T merge(T entity) { return (T) getSession().merge(entity); } public final void delete(T entity) { if (entity.getId() != null) { T entity = getById(id); entity.setDeleted(true); } } public T getById(Long id) { return (T) getSession().get(getEntityClass(), id); } public List<T> getAll() { return createCriteria().list(); } public List<T> getAll(String propertyName, SortOrder order) { return createCriteria() .addOrder(order == SortOrder.ASC ? Order.asc(propertyName) : Order.desc(propertyName)) .list(); } public List<T> getAllWithPagination(int start, int count, String sortPropertyName, boolean sortAscendingOrder) { return createCriteria() .setFirstResult(start) .setMaxResults(count) .addOrder(sortAscendingOrder ? Order.asc(sortPropertyName) : Order.desc(sortPropertyName)) .list(); } public int count() { Criteria criteria = createCriteria(); criteria.setProjection(Projections.rowCount()); return ((Number) criteria.list().get(0)).intValue(); } public Class<T> getEntityClass() { return this.entityClass; } protected Session getSession() { return sessionFactory.getCurrentSession(); } protected Criteria createCriteria() { return getSession().createCriteria(getEntityClass()); } }
Une fois cette infrastructure mise en place, il est extrêmement simple de définir un DAO pour notre entité Person :
public interface PersonDao extends GenericEntityDao<Person> { // Aucune méthode additionnelle nécessaire }
@Repository public class HibernatePersonDao extends HibernateGenericEntityDao<Person> implements PersonDao { public HibernatePersonDao() { super(Person.class); } }
Facile, n'est-ce pas ? Mais il nous manque encore quelquechose.
Vous vous rappelez le début de cet article ? En examinant le modèle Wicket, je vous faisais remarquer que la seule chose qui changeait vraiment d'un modèle à l'autre était le type de l'entité et celui du DAO associé, le second étant normalement déductible du premier.
Mais déductible comment ? Comment, concrètement, récupérer le DAO correspondant à une Entité donnée ? Je pense que vous aurez reconnu là le domaine d'application du design pattern Factory.
La DaoFactory que je vous présente ici s'appuie sur Spring pour l'auto-détection des DAO présents dans l'application, et sur la méthode getEntityClass() des DAO en question pour déterminer à quelle Entité les associer. La Factory ainsi obtenue est entièrement dynamique, ne nécessitant aucune intervention manuelle lorsque l'application évoluera.
public interface DaoFactory { <T extends GenericEntity> GenericEntityDao<T> getDaoFor(Class<T> entityClass); }
@Component public class HibernateDaoFactory implements ApplicationListener<ContextRefreshedEvent>, DaoFactory { private Map<Class<? extends GenericEntity>, GenericEntityDao<?>> daos = new HashMap<Class<? extends GenericEntity>, GenericEntityDao<?>>(); public void onApplicationEvent(ContextRefreshedEvent event) { ApplicationContext ctx = event.getApplicationContext(); daos.clear(); Map<String, GenericEntityDao> detectedDaos = ctx.getBeansOfType(GenericEntityDao.class); for (GenericEntityDao dao : detectedDaos.values()) { daos.put(dao.getEntityClass(), dao); } } public <T extends GenericEntity> GenericEntityDao<T> getDaoFor(Class<T> entityClass) { return (GenericEntityDao<T>) daos.get(entityClass); } }
Notre Factory écoute les événements de type ContextRefreshed, qui signalent que Spring a fini de scanner les composants et de construire son registre de beans. Il ne lui reste alors plus qu'à récupérer les différents DAOs détectés et à construire sa map interne Entité → Dao.
Notez que la DaoFactory est elle-même un composant Spring, il sera donc facile de l'injecter dans, au hasard, nos modèles Wicket via l'annotation @SpringBean.
Vous noterez encore une fois une limitation des Generics, qui ne permettent pas de "lier" les types des clés et des valeurs de la Map...
La couche présentation
Nous arrivons enfin à la couche de présentation, et aux modèles Wicket.
GenericEntityModel
Grâce à la DaoFactory et à la présence tout au long de la chaîne des types paramétrés de la forme <T extends GenericEntity>, il nous est désormais possible de réécrire notre modèle Wicket sous la forme suivante :
public class GenericEntityModel<T extends GenericEntity> extends LoadableDetachableModel<T> { @SpringBean private DaoFactory daoFactory; private Class<T> entityClass; private Long entityId; public GenericEntityModel(Class<T> entityClass, Long entityId) { this.entityClass = entityClass; this.entityId = entityId; InjectorHolder.getInjector().inject(this); } protected T load() { return getEntityDao().getById(entityId); } protected GenericEntityDao<T> getEntityDao() { return daoFactory.getDaoFor(entityClass); } }
Et nous pouvons l'utiliser partout où un simple modèle d'entité est requis, par exemple dans un lien inter-pages :
add(new Link<Void>("personDetailsLink") { public void onClick() { final GenericEntityModel<Person> personModel = new GenericEntityModel<Person>(Person.class, personId); setResponsePage(new PersonDetailsPage(personModel)); } });
Pratique, non ? Et finis les copier-coller ou les classes anonymes utilisés jusque-là.
GenericEntityListModel
Mais voyons maintenant d'autres types de modèles génériques, comme par exemple un modèle de liste d'entités.
public class GenericEntityListModel<T extends GenericEntity> extends LoadableDetachableModel<List<T>> { @SpringBean private DaoFactory daoFactory; private Class<T> entityClass; private String sortProperty; private SortOrder sortOrder; public GenericEntityListModel(Class<T> entityClass, String sortProperty, SortOrder sortOrder) { this.entityClass = entityClass; this.sortProperty = sortProperty; this.sortOrder = sortOrder; InjectorHolder.getInjector().inject(this); } protected List<T> load() { if (sortProperty == null) { return getEntityDao().getAll(); } else { return getEntityDao().getAll(sortProperty, sortOrder); } } protected GenericEntityDao<T> getEntityDao() { return daoFactory.getDaoFor(entityClass); } }
Ce modèle est idéalement utilisé avec une PropertyListView ou un DropDownChoice :
DropDownChoice<PErson> personChoice = new DropDownChoice<Person>("chosenPerson", new GenericEntityListModel<Person>(Person.class)); personChoice.setChoiceRenderer( ... ); add(personChoice);
GenericDataProvider
Pour terminer, voyons le GenericDataProvider, une implémentation générique de l'interface DataProvider utilisée par le composant DataTable.
Cette interface est généralement assez pénible à implémenter car il faut gérer la pagination et le tri, et renvoyer un itérateur, mais nous avons été prévoyants et notre GenericDao fournit toutes ces fonctionnalités.
public class GenericDataProvider<T extends GenerictEntity> extends SortableDataProvider<T> { @SpringBean private DaoFactory daoFactory; private Class<T> entityClass; public GenericDataProvider(Class<T> entityClass, String defaultSortProperty) { this.entityClass = entityClass; InjectorHolder.getInjector().inject(this); setSort(new SortParam(defaultSortProperty, true)); } @Override public Iterator<? extends T> iterator(int first, int count) { String sortProperty = getSort().getProperty(); return getEntityDao().getAllWithPagination(first, count, sortProperty, getSort().isAscending()).iterator(); } @Override public int size() { return getEntityDao().count(); } @Override public IModel<T> model(T entity) { return getEntityModel(entity); } protected GenericEntityDao<T> getEntityDao() { return daoFactory.getDaoFor(entityClass); } protected GenericEntityModel<T> getEntityModel(T entity) { return new GenericEntityModel<T>(entityClass, entity); } }
Il ne reste plus qu'à déclarer la DataTable en faisant usage :
GenericDataProvider<Person> dataProvider = new GenericDataProvider<Person>(Person.class, "lastName"); IColumn[] tableColumns = ... int nbRowsPerPage = ... DataTable<Person> peopleTable = new DataTable<Person>("people", tableColumns, dataProvider, rowsPerPage); peopleTable.addTopToolbar(new AjaxFallbackHeadersToolbar(peopleTable,dataProvider) peopleTable.addBottomToolbar(new NoRecordsToolbar(peopleTable)); peopleTable.addBottomToolbar(new AjaxNavigationToolbar(peopleTable)); add(peopleTable);
Conclusion
Nous voici enfin à la conclusion de cet article, merci de l'avoir lu jusqu'au bout !
J'espère avoir montré qu'avec un peu de préparation et une architecture générique, il était possible de simplifier et d'automatiser assez largement la gestion des modèles Wicket.
Ce système n'est évidemment pas parfait et n'est pas prévu pour couvrir l'intégralité des cas, mais suffit normalement pour les fameux 80% les plus courants.
Les techniques présentées ici dépassent évidemment le simple cadre de Wicket, mais l'association Hibernate/Wicket, huilée par les Generics, offre une plateforme puissante et productive que j'apprécie particulièrement.
A bientôt pour d'autres articles !


Commentaires
Pas parfait, effectivement (on perd la séparation en couches et surtout la couche de services métier, très importante dans une conception modulaire), mais le principe est particulièrement intéressant. Merci pour l'article.
article intéressant!! merci
mais ne serait il pas plus intéressant de faire de la classe GenericEntityDao<T> juste une marker interface, ce qui, je pense donne plus de flexibilité et de clarté. Permettre aux classes qui implémentent cette interface de déclarer les méthodes qui les intéresses aux lieux de "trimballer" plusieurs méthodes même si celle si sont réduite au strictes minimum du besoin?
En plus en avec cette interface, si un changement parvient toutes les classes qui les implémentes seront impactées.
merci
Merci pour cet article très complet. Cela manquait dans la "littérature web" sur Wicket.
Nous avons d'ailleurs une architecture très similaire, mais tu nous donnes quelques idées d'améliorations...
Le seul détail que je ne saisis pas, c'est pourquoi tu changes l'état du flag "deleted" pour supprimer une entité. Une suppression effective dans la base te pose-t-elle certaines contraintes ? Et ensuite, ne faut-il pas prendre en compte l'état de ce flag pour établir la liste des entités ?
@Shadowlaw : Remarque intéressante, mais tout l'intérêt du GenericEntityDao est justement de factoriser tout le code "boiler-plate" standard : création, suppression, liste, etc. La liste des fonctionnalités couvertes reste néanmoins réduite afin de limiter les risques d'impacts que vous évoquez, en cas de modification ou d'évolution.
Quant aux DAO spécifiques, qui dérivent/héritent de ce DAO générique, ils peuvent ensuite rajouter leurs propres méthodes. Les possibilités d'extension restent donc très importantes.
@adriclad : Il est très fréquent que, pour des raisons de sécurité (pour éviter de perdre irrémédiablement des données) ou d'audit, les suppressions physiques soient interdites en base. Dans ce cas, un simple flag peut remplir ce rôle.
Ensuite, lors des opérations de requêtage ou de mise à jour, il faut effectivement filtrer les entités marquées comme "supprimées". J'ai volontairement laissé ce point technique hors du scope de l'article, mais sachez que c'est facilement réalisable de manière automatique grâce aux Filtres d'Hibernate.
GenericEntity n'est pas vraiment générique!
comment s'arrange-t-on si on veut gérer des "sous-entités" avec une clef qui n'est pas restreinte au type Long c'est-à dire des sous entités héritant d'une telle classe:
import java.io.Serializable;
/**
public abstract class GenericEntity<KEY /*extends Serializable & Comparable<KEY>*/ /*optionnel, mais ne change pas du tout la question*/> implements Serializable {
private static final long serialVersionUID = 1L;
private KEY key;
protected GenericEntity() {
super();
}
protected GenericEntity(final KEY key) {
super();
this.key = key;
}
public final KEY getKey() {
return this.key;
}
public final void setKey(final KEY key) {
this.key = key;
}
}
je suis preneur pour toute réponse me donnant une bonne piste, je pose la question depuis un bon moment mais sans réponse, comme si les types génériques étaient nouveaux en Java.