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é) :
- <?xml version="1.0" encoding="utf-8" ?>
- <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="true" >
- <class name="NewslettersConnector.Model.Newsletters, NewslettersConnector" table="NEWSLETTERS">
- <id name="Id" column="ID" type="System.Int16" access="property">
- <generator class="identity"></generator>
- </id>
- <property name="ProprietaireId" column="nl_propid" not-null="true" type="System.Int64" access="property"/>
- <property name="Email" column="nl_email" not-null="true" type="System.String" access="property"/>
- <property name="Libelle" column="nl_libelle" not-null="false" type="System.String" access="property"/>
- <property name="Description" column="nl_description" not-null="false" type="StringClob" access="property"/>
- <property name="DateCreation" column="nl_datecreation" not-null="true" type="System.DateTime" access="property"/>
- <bag name="Abonnements" fetch="select" access="property" lazy="true" inverse="true">
- <key column="nl_id"/>
- <one-to-many class="NewslettersConnector.Model.NewslettersAbonnements, NewslettersConnector" />
- </bag>
- <bag name="AbonnementsExternes" fetch="select" access="property" lazy="true" inverse="true"
- where="abo_typeabonne=1 and ind_id is not null">
- <key column="nl_id"/>
- <one-to-many class="NewslettersConnector.Model.NewslettersAbonnements, NewslettersConnector" />
- </bag>
- <bag name="AbonnementsInternes" fetch="select" access="property" lazy="true" inverse="true"
- where="abo_typeabonne=2 and ind_id is not null">
- <key column="nl_id"/>
- <one-to-many class="NewslettersConnector.Model.NewslettersAbonnements, NewslettersConnector" />
- </bag>
- </class>
- </hibernate-mapping>
La classe sérialisée pour le mapping :
- namespace NewslettersConnector.Model
- {
- [Serializable]
- public class Newsletters
- {
- private Int16 _id;
- public virtual Int16 Id
- {
- get { return _id; }
- set { _id = value; }
- }
- private long _proprioid;
- public virtual long ProprietaireId
- {
- get { return _proprioid; }
- set { _proprioid = value; }
- }
- private string _email;
- public virtual string Email
- {
- get { return _email; }
- set { _email = value; }
- }
- private string _libelle;
- public virtual string Libelle
- {
- get { return _libelle; }
- set { _libelle = value; }
- }
- private string _description;
- public virtual string Description
- {
- get { return _description; }
- set { _description = value; }
- }
- private DateTime _datecreation;
- public virtual DateTime DateCreation
- {
- get { return _datecreation; }
- set { _datecreation = value; }
- }
- private IList<NewslettersAbonnements> _abonnements;
- /// <summary>
- /// Abonnements (tout type) - sérialisé par NHibernate, voir mapping Newsletters.hbm.xml
- /// </summary>
- [XmlIgnore]
- public virtual IList<NewslettersAbonnements> Abonnements
- {
- get { return _abonnements; }
- set { _abonnements = value; }
- }
- private IList<NewslettersAbonnements> _abonnementsexternes;
- /// <summary>
- /// AbonnementsExternes - sérialisé par NH, voir mapping Newsletters.hbm.xml
- /// </summary>
- [XmlIgnore]
- public virtual IList<NewslettersAbonnements> AbonnementsExternes
- {
- get { return _abonnementsexternes; }
- set { _abonnementsexternes = value; }
- }
- private IList<NewslettersAbonnements> _abonnementsinternes;
- /// <summary>
- /// AbonnementsInternes - sérialisé par NH, voir mapping Newsletters.hbm.xml
- /// </summary>
- [XmlIgnore]
- public virtual IList<NewslettersAbonnements> AbonnementsInternes
- {
- get { return _abonnementsinternes; }
- set { _abonnementsinternes= value; }
- }
- }
- }
L'interface du contrôleur qui sera implémentée :
- namespace NewslettersConnector
- {
- public interface INewslettersManager
- {
- Newsletters getNewsletter(int nlid);
- Newsletters getNewsletter(string email);
- void saveAbonnement(NewslettersAbonnements abo);
- void deleteAbonnement(NewslettersAbonnements abo);
- NewslettersAbonnements getAbonnement(int nlid, long indId);
- }
- }
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.