Proposition d'architecture générique avec Hibernate, Spring et Wicket


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 :

  1. private class EmployeeModel extends LoadableDetachableModel<Employee> {
  2.  
  3. @Autowired
  4. private EmployeeDao employeeDao;
  5. private final Long id;
  6.  
  7. public EmployeeModel(Long id) {
  8. this.id = id;
  9. }
  10.  
  11. @Override
  12. protected Employee load() {
  13. return employeeDao.getById(id);
  14. }
  15.  
  16. }

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 :

  1. @MappedSuperclass
  2. public abstract class GenericEntity implements Serializable {
  3.  
  4. @Id
  5. @GeneratedValue(strategy = GenerationType.SEQUENCE)
  6. private Long id;
  7.  
  8. @Version
  9. private Long version;
  10.  
  11. @Basic(optional = true)
  12. private boolean deleted = false;
  13.  
  14. }

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 :

  1. @Entity
  2. public class Person extends GenericEntity {
  3.  
  4. private String firstName;
  5. private String lastName;
  6. private String email;
  7.  
  8. }

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.

  1. public interface GenericEntityDao<T extends GenericEntity> {
  2.  
  3. T save(T entity);
  4. T merge(T entity);
  5. void delete(T entity);
  6.  
  7. T getById(Long id);
  8.  
  9. List<T> getAll();
  10. List<T> getAll(String propertyName, SortOrder order);
  11. List<T> getAllWithPagination(int start, int count);
  12. List<T> getAllWithPagination(int start, int count, String sortPropertyName, boolean sortAscendingOrder);
  13.  
  14. int count();
  15.  
  16. Class<T> getEntityClass();
  17.  
  18. }

Voici l'implémentation Hibernate correspondante :

  1. @Repository
  2. @Transactional
  3. public abstract class HibernateGenericEntityDao<T extends GenericEntity> implements GenericEntityDao<T> {
  4.  
  5. @Autowired(required = true)
  6. private SessionFactory sessionFactory;
  7.  
  8. private Class<T> entityClass;
  9.  
  10. public HibernateGenericEntityDao(Class<T> entityClass) {
  11. this.entityClass = entityClass;
  12. }
  13.  
  14. public T save(T entity) {
  15. getSession().saveOrUpdate(entity);
  16. return entity;
  17. }
  18.  
  19. public T merge(T entity) {
  20. return (T) getSession().merge(entity);
  21. }
  22.  
  23. public final void delete(T entity) {
  24. if (entity.getId() != null) {
  25. T entity = getById(id);
  26. entity.setDeleted(true);
  27. }
  28. }
  29.  
  30. public T getById(Long id) {
  31. return (T) getSession().get(getEntityClass(), id);
  32. }
  33.  
  34. public List<T> getAll() {
  35. return createCriteria().list();
  36. }
  37.  
  38. public List<T> getAll(String propertyName, SortOrder order) {
  39. return createCriteria()
  40. .addOrder(order == SortOrder.ASC ? Order.asc(propertyName) : Order.desc(propertyName))
  41. .list();
  42. }
  43.  
  44. public List<T> getAllWithPagination(int start, int count, String sortPropertyName, boolean sortAscendingOrder) {
  45. return createCriteria()
  46. .setFirstResult(start)
  47. .setMaxResults(count)
  48. .addOrder(sortAscendingOrder ? Order.asc(sortPropertyName) : Order.desc(sortPropertyName))
  49. .list();
  50. }
  51.  
  52. public int count() {
  53. Criteria criteria = createCriteria();
  54. criteria.setProjection(Projections.rowCount());
  55. return ((Number) criteria.list().get(0)).intValue();
  56. }
  57.  
  58. public Class<T> getEntityClass() {
  59. return this.entityClass;
  60. }
  61.  
  62. protected Session getSession() {
  63. return sessionFactory.getCurrentSession();
  64. }
  65.  
  66. protected Criteria createCriteria() {
  67. return getSession().createCriteria(getEntityClass());
  68. }
  69.  
  70. }

Une fois cette infrastructure mise en place, il est extrêmement simple de définir un DAO pour notre entité Person :

  1. public interface PersonDao extends GenericEntityDao<Person> {
  2. // Aucune méthode additionnelle nécessaire
  3. }
  1. @Repository
  2. public class HibernatePersonDao extends HibernateGenericEntityDao<Person> implements PersonDao {
  3.  
  4. public HibernatePersonDao() {
  5. super(Person.class);
  6. }
  7.  
  8. }

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.

  1. public interface DaoFactory {
  2. <T extends GenericEntity> GenericEntityDao<T> getDaoFor(Class<T> entityClass);
  3. }
  1. @Component
  2. public class HibernateDaoFactory implements ApplicationListener<ContextRefreshedEvent>, DaoFactory {
  3.  
  4. private Map<Class<? extends GenericEntity>, GenericEntityDao<?>> daos
  5. = new HashMap<Class<? extends GenericEntity>, GenericEntityDao<?>>();
  6.  
  7. public void onApplicationEvent(ContextRefreshedEvent event) {
  8. ApplicationContext ctx = event.getApplicationContext();
  9. daos.clear();
  10. Map<String, GenericEntityDao> detectedDaos = ctx.getBeansOfType(GenericEntityDao.class);
  11. for (GenericEntityDao dao : detectedDaos.values()) {
  12. daos.put(dao.getEntityClass(), dao);
  13. }
  14. }
  15.  
  16. public <T extends GenericEntity> GenericEntityDao<T> getDaoFor(Class<T> entityClass) {
  17. return (GenericEntityDao<T>) daos.get(entityClass);
  18. }
  19. }

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 :

  1. public class GenericEntityModel<T extends GenericEntity> extends LoadableDetachableModel<T> {
  2.  
  3. @SpringBean
  4. private DaoFactory daoFactory;
  5.  
  6. private Class<T> entityClass;
  7. private Long entityId;
  8.  
  9. public GenericEntityModel(Class<T> entityClass, Long entityId) {
  10. this.entityClass = entityClass;
  11. this.entityId = entityId;
  12. InjectorHolder.getInjector().inject(this);
  13. }
  14.  
  15. protected T load() {
  16. return getEntityDao().getById(entityId);
  17. }
  18.  
  19. protected GenericEntityDao<T> getEntityDao() {
  20. return daoFactory.getDaoFor(entityClass);
  21. }
  22.  
  23. }

Et nous pouvons l'utiliser partout où un simple modèle d'entité est requis, par exemple dans un lien inter-pages :

  1. add(new Link<Void>("personDetailsLink") {
  2. public void onClick() {
  3. final GenericEntityModel<Person> personModel = new GenericEntityModel<Person>(Person.class, personId);
  4. setResponsePage(new PersonDetailsPage(personModel));
  5. }
  6. });

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.

  1. public class GenericEntityListModel<T extends GenericEntity> extends LoadableDetachableModel<List<T>> {
  2.  
  3. @SpringBean
  4. private DaoFactory daoFactory;
  5.  
  6. private Class<T> entityClass;
  7. private String sortProperty;
  8. private SortOrder sortOrder;
  9.  
  10. public GenericEntityListModel(Class<T> entityClass, String sortProperty, SortOrder sortOrder) {
  11. this.entityClass = entityClass;
  12. this.sortProperty = sortProperty;
  13. this.sortOrder = sortOrder;
  14. InjectorHolder.getInjector().inject(this);
  15. }
  16.  
  17. protected List<T> load() {
  18. if (sortProperty == null) {
  19. return getEntityDao().getAll();
  20. } else {
  21. return getEntityDao().getAll(sortProperty, sortOrder);
  22. }
  23. }
  24.  
  25. protected GenericEntityDao<T> getEntityDao() {
  26. return daoFactory.getDaoFor(entityClass);
  27. }
  28.  
  29. }

Ce modèle est idéalement utilisé avec une PropertyListView ou un DropDownChoice :

  1. DropDownChoice<PErson> personChoice = new DropDownChoice<Person>("chosenPerson", new GenericEntityListModel<Person>(Person.class));
  2. personChoice.setChoiceRenderer( ... );
  3. 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.

  1. public class GenericDataProvider<T extends GenerictEntity> extends SortableDataProvider<T> {
  2.  
  3. @SpringBean
  4. private DaoFactory daoFactory;
  5.  
  6. private Class<T> entityClass;
  7.  
  8. public GenericDataProvider(Class<T> entityClass, String defaultSortProperty) {
  9. this.entityClass = entityClass;
  10. InjectorHolder.getInjector().inject(this);
  11. setSort(new SortParam(defaultSortProperty, true));
  12. }
  13.  
  14. @Override
  15. public Iterator<? extends T> iterator(int first, int count) {
  16. String sortProperty = getSort().getProperty();
  17. return getEntityDao().getAllWithPagination(first, count, sortProperty, getSort().isAscending()).iterator();
  18. }
  19.  
  20. @Override
  21. public int size() {
  22. return getEntityDao().count();
  23. }
  24.  
  25. @Override
  26. public IModel<T> model(T entity) {
  27. return getEntityModel(entity);
  28. }
  29.  
  30. protected GenericEntityDao<T> getEntityDao() {
  31. return daoFactory.getDaoFor(entityClass);
  32. }
  33.  
  34. protected GenericEntityModel<T> getEntityModel(T entity) {
  35. return new GenericEntityModel<T>(entityClass, entity);
  36. }
  37. }

Il ne reste plus qu'à déclarer la DataTable en faisant usage :

  1. GenericDataProvider<Person> dataProvider = new GenericDataProvider<Person>(Person.class, "lastName");
  2. IColumn[] tableColumns = ...
  3. int nbRowsPerPage = ...
  4. DataTable<Person> peopleTable = new DataTable<Person>("people", tableColumns, dataProvider, rowsPerPage);
  5. peopleTable.addTopToolbar(new AjaxFallbackHeadersToolbar(peopleTable,dataProvider)
  6. peopleTable.addBottomToolbar(new NoRecordsToolbar(peopleTable));
  7. peopleTable.addBottomToolbar(new AjaxNavigationToolbar(peopleTable));
  8. 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

1. Le Vendredi 18 mars 2011, 16:01 par yannick555

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.

2. Le Lundi 21 mars 2011, 11:01 par shadowlaw

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

3. Le Mardi 22 mars 2011, 01:30 par adriclad

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 ?

4. Le Mardi 22 mars 2011, 14:53 par Olivier Croisier

@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.

5. Le Vendredi 10 juin 2011, 13:05 par bdp99

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;

/**

* @author bdp99
*/

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.

Fil des commentaires de ce billet