N-bases

Introduction

Parmi les avantages d'un ORM, nous retrouvons le support de multi-bases : la base devrait être un module abstrait, ainsi, le code ne devrait pas être développé et adapté selon la base sous-jacente. Les ORM, et notamment NHibernate, permettent de s'affranchir de cette partie (requêtes SQL, connexion, ...) et de rendre transparent la persistance des données en base.

NHibernate met en place des Dialects, qui grâce à eux, vous permettront, en utilisant l'API Criteria ou le langage HQL, de ne plus écrire de SQL. NH traduit votre requête objet dans le langage SQL de la base utilisée (qui est bien souvent non standard entre les différentes bases), ce qui abstrait totalement quelle base est utilisée.

Comment faire ?

NHibernate propose de charger une configuration selon le type de base utilisé. En changeant dynamiquement le fichier de configuration, on peut ainsi changer le fournisseur d'accès à la base.

Dans notre exemple, 3 configurations sont mises en place :

SQLite, contenu dans le fichier SQLite-nhihernate.config. Ce fichier précise quel Dialect utilisé, ici SQLite3, et divers paramètres, comme l'emplacement le base (connection_string) et la définition du modèle sous forme de fichier XML (MyDocumentRules : x.hbm.xml : les fichiers de mapping de nos entités) :

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" >
  3. <session-factory name="mymedias">
  4. <property name="dialect">NHibernate.Dialect.SQLiteDialect</property>
  5. <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
  6. <property name="connection.driver_class">NHibernate.Driver.SQLite20Driver</property>
  7. <property name="connection.connection_string">Data Source=|DataDirectory|mydb.s3db;Version=3</property>
  8. <property name="connection.isolation">ReadCommitted</property>
  9. <property name="query.substitutions">true 1, false 0</property>
  10. <!-- HBM Mapping Files -->
  11. <mapping assembly="MyDocumentRules" />
  12. </session-factory>
  13. </hibernate-configuration>

MySQL : idem mais pour MySQL :

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" >
  3. <session-factory name="mymedias">
  4. <property name="dialect">NHibernate.Dialect.MySQLDialect</property>
  5. <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
  6. <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
  7. <property name="connection.connection_string">Server=localhost;Database=mymedias;User ID=root;Password=mypasswd</property>
  8. <property name="query.substitutions">true 1, false 0</property>
  9. <!-- HBM Mapping Files -->
  10. <mapping assembly="MyDocumentRules" />
  11. </session-factory>
  12. </hibernate-configuration>

MSSQL 2005 Express :

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" >
  3. <session-factory name="mymedias">
  4. <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
  5. <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
  6. <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
  7. <property name="connection.connection_string">Data Source=.\SQLEXPRESS;Pooling=true; Max Pool Size=2;persist security info=False;User Instance=True;AttachDBFilename=|DataDirectory|mymediasthec.mdf;Trusted_Connection=yes;</property>
  8. <property name="connection.isolation">ReadCommitted</property>
  9. <!--<property name="default_schema">
  10. mymedias.dbo</property>-->
  11. <property name="query.substitutions">true 1, false 0</property>
  12. <!-- HBM Mapping Files -->
  13. <mapping assembly="MyDocumentRules" />
  14. </session-factory>
  15. </hibernate-configuration>

Il nous reste plus qu'à charger (selon une clé positionnée dans l'application) le fichier de configuration que nous souhaitons utiliser, ce qui ressemblera à :

  1. public class NHibernateSessionFactory
  2. {
  3. public static NHibernate.ISessionFactory GetSessionFactory(string configpath)
  4. {
  5. Configuration cfg = new Configuration();
  6. cfg.Configure(configpath);
  7. return cfg.BuildSessionFactory();
  8. }
  9. }
  10.  
  11. public class ContainerFactory
  12. {
  13. public static NHibernate.ISessionFactory LaFactory
  14. {
  15. get
  16. {
  17. NHibernate.ISessionFactory laFactory = NHibernateSessionFactory.GetSessionFactory(ConfigurationManager.AppSettings["ConfigDBPath"]);
  18. return laFactory;
  19. }
  20. }
  21. }

Simple, portable et pratique.

Composition ou le one-to-one

Maintenant, nous allons ajouter une pochette à nos médias (couverture de livre, pochette de CD, ...). Cette relation entre un média et sa pochette s'effectue grâce à une composition : autrement dit, une pochette n'existe qu'au travers un média (un livre, un cd, ...), si le média n'existe plus, la pochette non plus (a contrario d'un auteur par exemple). Cela se représente par le diagramme suivant :

Composition

Sous NHibernate, cette relation s'effectue grâce à deux moyens, du côté du média (many-to-one) et du côté de la pochette (one-to-one).

Média, on aura la définition suivante dans le media.hbm.xml :

  1. <many-to-one name="Pochette" class="MyConnector.Model.Pochette, MyConnector" column="pochette_id"
  2. not-null="false" lazy="false" cascade="all-delete-orphan" />

J'ai mis à lazy="false" car lors de l'accès à une pochette, NH me ramène le proxy (constitué uniquement de l'id) et non l'objet, pas encore trouvé d'explications. cascade="all-delete-orphan" permettra de supprimer automatiquement la pochette dès que le média sera supprimé.

Pour la pochette :

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" auto-import="true" >
  3. <class name="MyConnector.Model.Pochette, MyConnector" table="pochettes">
  4. <id name="Id" type="System.Int32">
  5. <generator class="foreign">
  6. <param name="property">Media</param>
  7. </generator>
  8. </id>
  9. <property name="Titre" column="title" not-null="false" type="System.String" access="property"/>
  10. <property name="Fichier" column="fileimage" not-null="true" type="System.String" access="property"/>
  11. <property name="SmallFichier" column="smallfile" not-null="false" type="System.String" access="property"/>
  12. <property name="Repertoire" column="dir" not-null="false" type="System.String" access="property"/>
  13. <property name="DateCreation" column="datecreation" not-null="true" type="System.DateTime" access="property"/>
  14.  
  15. <one-to-one name="Media" class="MyConnector.Model.Media, MyConnector" constrained="true" />
  16. </class>
  17. </hibernate-mapping>

La relation one-to-one pour la pochette (grâce à generator class="foreign") implique qu'une pochette aura le même id que le média, les 2 entités se partagent la même clé primaire.

Le constrained="true" permet d'induire une contrainte (et cela pétera votre appli. si elle n'est pas respectée...) et aussi selon Ayende (voir le commentaire de Sergey Koshcheyev) de construire une requête en inner join au lieu d'une left outer join, plus conforme pour ce cas.

On veillera à bien supprimer la pochette lors de la suppression d'un média (cascade delete), car cela constitue une contrainte d'intégrité pour NH (si le SGBD n'implémente pas les contraintes, on le fera donc du point du vue logiciel) : l'id du média correspond à l'id de la pochette, on pourra y trouver des avantages et des inconvénients à cette technique.

Conteneur IoC Castle Windsor

Il existe plusieurs conteneurs IoC : Spring, Windsor, ... Ce type de framework va nous servir à instancier des objets en s'appuyant sur des interfaces (abstraction) qu'auront implémentées des classes. Le framework s'intercale entre la couche de contrats (représentée par les interfaces) et l'implémentation, ce qui permettra d'introduire un découplage entre les couches : l'application Web ne connaît pas a priori quelle implémentation sera utilisée, elle n'utilisera que des implémentations d'interfaces (explicites).

L'avantage (ou inconvénients pour certains) du framework, c'est l'utilisation de fichiers (XML) pour décrire les implémentations à utiliser et l'injection d'objets le cas échéant dans les implémentations.

On veut remplacer l'injection que nous faisions à la main grâce à :

  • IDALContainerFactory : création de la session NHibernate, obtention du container utilisé par l'ORM
  • IDALContainer : container utilisé par l'ORM, qui pourrait changer (on utilise aussi un ORM maison : dsMap)
  • IManagerControler : factory pour l'instanciation des contrôleurs

tout ceci va être remplacé par Windsor et son fichier de configuration CastleWindsorComponents.config :

  1. <?xml version="1.0"?>
  2. <configuration>
  3. <components>
  4. <!-- container ORM -->
  5. <component id="DALContainer"
  6. type="MyDocumentRules.DALContainer, MyDocumentRules"
  7. service="MyConnector.Interfaces.IDALContainer, MyConnector">
  8. </component>
  9.  
  10. <!-- Managers / contrôleurs -->
  11. <component id="mediasManager"
  12. type="MyDocumentRules.MediasManager, MyDocumentRules"
  13. service="MyConnector.Interfaces.IMediasManager, MyConnector"
  14. lifestyle="transient">
  15. <!--lifestyle="custom"
  16.   customLifestyleType="MyConnector.Util.HttpContextLifestyleManager, MyConnector">
  17. -->
  18.  
  19. <parameters>
  20. <container>${DALContainer}</container>
  21. </parameters>
  22. </component>
  23.  
  24. <component id="auteursManager"
  25. type="MyDocumentRules.AuteurManager, MyDocumentRules"
  26. service="MyConnector.Interfaces.IAuteursManager, MyConnector"
  27. lifestyle="transient">
  28. <parameters>
  29. <container>${DALContainer}</container>
  30. </parameters>
  31. </component>
  32. </components>
  33. </configuration>

Tous les objets à instancier sont décrits dans ce fichier sous la forme :

  • d'un type du service (interface) : attribut service
  • d'une implémentation à utiliser : attribut type
  • d'un paramètre éventuel (à injecter ou à passer pour une valeur par exemple) lors de l'instanciation : les contrôleurs mediaManager et auteurManager ont besoin du contexte utilisé par l'ORM (qui reste abstrait au niveau de la couche présentation). Ce paramètre est dynamique et prendra la forme ${DALContainer}, Windsor utilisera le composant décrit dans le fichier, identifié par DALContainer. (component id="DALContainer")

Par défaut, les composants instanciés seront des singletons. Lors de l'utilisation d'un singleton, le client est vivement poussé à savoir ce qui se passe exactement dans l'objet (comment seront gérées les variables : sont-elles bien réinitialisées ou utilisées au bon endroit ?...). On peut jouer sur la forme d'instanciation grâce à l'attribut lifestyle, transient permet de créer un nouvel objet à chaque appel, plus rassurant pour le client de l'objet (ici le Web).

L'aide sur les possibilités de la configuration de Castle Windsor.

Ajoutons une classe utilitaire IoC qui encapsulera l'appel à Windsor :

  1. namespace MyConnector.Util
  2. {
  3. public static class IoC
  4. {
  5. private static IWindsorContainer _container = new WindsorContainer(new XmlInterpreter());
  6.  
  7. public static T Resolve<T>()
  8. {
  9. return _container.Resolve<T>();
  10. }
  11.  
  12. public static object Resolve(Type service)
  13. {
  14. return _container.Resolve(service);
  15. }
  16.  
  17. public static void Reset()
  18. {
  19. if (_container != null)
  20. {
  21. _container.Dispose();
  22. _container = null;
  23. }
  24. }
  25. }
  26. }

ainsi, un simple

  1. IMediasManager mediaManager = IoC.Resolve<IMediasManager>())) as IMediasManager;

nous donnera accès au manager fraichement instancié.

On pourra mettre cela dans la classe utilitaire MyWebAppBasePage qu'utilisent les pages. Ici l'utilisation de HttpContext.Current.Items garantit qu'on utilisera le même objet pour toute la requête HTTP pour le même connecté, et non un thread éventuellement issu d'une autre requête HTTP (ASP.NET utilisant un pool de threads), ce qui peut provoquer des effets de bords (des propriétés initialisées pour le connecté X, puis transférées pour le connecté Y, on image le problème). Voir notamment ce très bon billet sur l'utilisation à éviter du ThreadStatic en environnement ASP.NET.


  1. public class MyWebAppBasePage : Page
  2. {
  3. #region Managers
  4.  
  5. public IMediasManager mediaManager
  6. {
  7. get
  8. {
  9. return (HttpContext.Current.Items["mediamgr"] ??
  10. (HttpContext.Current.Items["mediamgr"] = IoC.Resolve<IMediasManager>())) as IMediasManager;
  11. }
  12. }
  13.  
  14. public IAuteursManager auteurManager
  15. {
  16. get
  17. {
  18. return (HttpContext.Current.Items["autmgr"] ??
  19. (HttpContext.Current.Items["autmgr"] = IoC.Resolve<IAuteursManager>())) as IAuteursManager;
  20. }
  21. }
  22.  
  23. #endregion
  24.  
  25. public void delImagePochette(Media Media)
  26. {
  27. if (Media != null &&
  28. Media.Pochette != null)
  29. {
  30. int id = Media.Pochette.Id;
  31. string fic = string.Format("{0}\\{1}", Server.MapPath("/" + Media.Pochette.Repertoire), Media.Pochette.Fichier);
  32. if (File.Exists(fic))
  33. File.Delete(fic);
  34. }
  35. }
  36.  
  37. protected override void OnInit(EventArgs e)
  38. {
  39. base.OnInit(e);
  40.  
  41. Unload += new EventHandler(MyWebAppBasePage_Unload);
  42. }
  43.  
  44. void MyWebAppBasePage_Unload(object sender, EventArgs e)
  45. {
  46. IoC.Resolve<IDALContainer>().ClearWorkingContext();
  47. }
  48. }

Sources

La démo est disponible sur Google code via SVN, à utiliser sous Visual Studio 2008, un F5 permettra de lancer l'application.

Prendre l'URL, un Checkout avec TortoiseSVN et c'est parti :

Je remercie mon jeune équipier AJN de m'avoir fait découvrir les choses suivantes :