ORM / NHibernate : mapper des sous-collections ou pas ?

Publié le 17 juin 2008 par Olivier Duval

Préambule

Actuellement, je gère un projet de mutualisation de newsletters, celles-ci seront intégrées à notre plateforme extranet, les abonnés gérés par des propriétaires. Les abonnés sont soit extraits de l'annuaire interne à notre extranet, soit externes à ce dernier et donc ajoutés manuellement.

Afin de gérer ce cas d'utilisation, nous aurons notamment besoin de connaître :

  • tous les abonnés,
  • les abonnés internes, issus de l'annuaire,
  • les abonnés externes, ajoutés manuellement par le propriétaire

On aura schématiquement le diagramme de classes suivant (merci à Dia) :

Question philosophique du jour

Pour gérer les abonnements, nous nous appuyons sur un contrôleur (ci-après), et j'ai eu un doute sur la méthode pour obtenir les sous-ensembles d'abonnés internes et externes : utiliser la capacité de l'ORM de le faire ou interroger le contrôleur (voir l'introduction aux architectures n-tiers) avec les méthodes adhoc ?

Explications

NHibernate, l'ORM que nous utilisons, permet de créer des collections d'objets avec des clauses where (et donc d'avoir des sous-ensembles d'objets de même type), dès lors, plus besoin d'avoir des méthodes (par exemple List<NewslettersAbonnements> listAbonnementsExternes(int nlid) et List<NewslettersAbonnements> listAbonnementsInternes(int nlid))) dans le contrôleur pour obtenir ce type de collections.

Après discussion avec les membres de l'équipe, nous sommes arrivés aux arguments suivants :

  • pour plus de clarté, la classe sérialisée par NHibernate devra contenir les commentaires qu'il faut (ie : comment sont générées ces collections, pour expliquer que ce n'est pas magique),
  • cela concerne des collections précises et peu d'objets,
  • le fait de créer des méthodes spécifiques pour obtenir les abonnements internes et externes apporte peu de valeur ajoutée et que cela surchargerait le contrôleur par des méthodes qui ne sont pas liées directement au cas d'utilisation,
  • on s'appuie sur la capacité de l'ORM pour obtenir les collections d'objets de façon simple, tout en exploitant sa capacité à gérer le lazy-loading

la conclusion fût : ok pour créer des collections de sous-ensembles dans le mapping, étonnant

Mapping

Alors comment faire pour arriver à ce que nous souhaitons faire ?

Le fichier de mapping .hbm.xml pour obtenir ces collections (parties bag AbonnementsExternes et AbonnementsInternes), on mettra bien évidemment ces collections en lazyloading afin de les charger que lorsqu'elles seront utilisées (ie : appel à la propriété) :

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="true" >
  3. <class name="NewslettersConnector.Model.Newsletters, NewslettersConnector" table="NEWSLETTERS">
  4. <id name="Id" column="ID" type="System.Int16" access="property">
  5. <generator class="identity"></generator>
  6. </id>
  7. <property name="ProprietaireId" column="nl_propid" not-null="true" type="System.Int64" access="property"/>
  8. <property name="Email" column="nl_email" not-null="true" type="System.String" access="property"/>
  9. <property name="Libelle" column="nl_libelle" not-null="false" type="System.String" access="property"/>
  10. <property name="Description" column="nl_description" not-null="false" type="StringClob" access="property"/>
  11. <property name="DateCreation" column="nl_datecreation" not-null="true" type="System.DateTime" access="property"/>
  12. <bag name="Abonnements" fetch="select" access="property" lazy="true" inverse="true">
  13. <key column="nl_id"/>
  14. <one-to-many class="NewslettersConnector.Model.NewslettersAbonnements, NewslettersConnector" />
  15. </bag>
  16. <bag name="AbonnementsExternes" fetch="select" access="property" lazy="true" inverse="true"
  17. where="abo_typeabonne=1 and ind_id is not null">
  18. <key column="nl_id"/>
  19. <one-to-many class="NewslettersConnector.Model.NewslettersAbonnements, NewslettersConnector" />
  20. </bag>
  21. <bag name="AbonnementsInternes" fetch="select" access="property" lazy="true" inverse="true"
  22. where="abo_typeabonne=2 and ind_id is not null">
  23. <key column="nl_id"/>
  24. <one-to-many class="NewslettersConnector.Model.NewslettersAbonnements, NewslettersConnector" />
  25. </bag>
  26. </class>
  27. </hibernate-mapping>

La classe sérialisée pour le mapping :

  1. namespace NewslettersConnector.Model
  2. {
  3. [Serializable]
  4. public class Newsletters
  5. {
  6. private Int16 _id;
  7. public virtual Int16 Id
  8. {
  9. get { return _id; }
  10. set { _id = value; }
  11. }
  12. private long _proprioid;
  13. public virtual long ProprietaireId
  14. {
  15. get { return _proprioid; }
  16. set { _proprioid = value; }
  17. }
  18. private string _email;
  19. public virtual string Email
  20. {
  21. get { return _email; }
  22. set { _email = value; }
  23. }
  24. private string _libelle;
  25. public virtual string Libelle
  26. {
  27. get { return _libelle; }
  28. set { _libelle = value; }
  29. }
  30. private string _description;
  31. public virtual string Description
  32. {
  33. get { return _description; }
  34. set { _description = value; }
  35. }
  36. private DateTime _datecreation;
  37. public virtual DateTime DateCreation
  38. {
  39. get { return _datecreation; }
  40. set { _datecreation = value; }
  41. }
  42. private IList<NewslettersAbonnements> _abonnements;
  43. /// <summary>
  44. /// Abonnements (tout type) - sérialisé par NHibernate, voir mapping Newsletters.hbm.xml
  45. /// </summary>
  46. [XmlIgnore]
  47. public virtual IList<NewslettersAbonnements> Abonnements
  48. {
  49. get { return _abonnements; }
  50. set { _abonnements = value; }
  51. }
  52. private IList<NewslettersAbonnements> _abonnementsexternes;
  53. /// <summary>
  54. /// AbonnementsExternes - sérialisé par NH, voir mapping Newsletters.hbm.xml
  55. /// </summary>
  56. [XmlIgnore]
  57. public virtual IList<NewslettersAbonnements> AbonnementsExternes
  58. {
  59. get { return _abonnementsexternes; }
  60. set { _abonnementsexternes = value; }
  61. }
  62. private IList<NewslettersAbonnements> _abonnementsinternes;
  63. /// <summary>
  64. /// AbonnementsInternes - sérialisé par NH, voir mapping Newsletters.hbm.xml
  65. /// </summary>
  66. [XmlIgnore]
  67. public virtual IList<NewslettersAbonnements> AbonnementsInternes
  68. {
  69. get { return _abonnementsinternes; }
  70. set { _abonnementsinternes= value; }
  71. }
  72. }
  73. }

L'interface du contrôleur qui sera implémentée :

  1. namespace NewslettersConnector
  2. {
  3. public interface INewslettersManager
  4. {
  5. Newsletters getNewsletter(int nlid);
  6. Newsletters getNewsletter(string email);
  7. void saveAbonnement(NewslettersAbonnements abo);
  8. void deleteAbonnement(NewslettersAbonnements abo);
  9. NewslettersAbonnements getAbonnement(int nlid, long indId);
  10. }
  11. }

Remarques

Dans la classe Newsletters, les propriétés sont en virtual, cela permet de profiter du lazy-loading le cas échéant. NHibernate pourra ainsi créer des objets internes qui hériteront de nos objets métiers, et dès lors, déclencher le chargement des collections lors de leur accès.

Une question d'un membre de l'équipe : vaut-il mieux utiliser les fichiers hbm.xml (et donc décrire le mapping sous forme XML) ou utiliser des attributs (comme le ferait ActiveRecord ou dsMap, notre ancien ORM) ?

Personnellement, autant que faire se peut, il est préférable de ne pas polluer la classe entité (ici Newsletters) par des attributs spécifiques à l'ORM, on se couperait d'une abstraction avec le framework de mapping. Si des attributs étaient utilisés, on se verrait obligés d'utiliser des classes DTO, transverses à toutes les couches, et de recopier les objets du mapping vers ces objets DTO, afin de garantir une abstraction, ce qui complique (recopie) et ajoute de la complexité (couche supplémentaire) au projet.

Sur ce, je vais me concentrer sur le match France-Italie, c'est mal parti...Ribery sorti sur blessure, carton rouge pour un autre joueur, pénalty...super...

Et n'oubliez pas : Firefox 3 record à battre.