6. Introduction à l’ORM NHibernate
Ce chapitre est une introduction succincte à NHibernate, l'équivalent pour .NET du framework Java Hibernate. Pour une introduction complète on pourra lire :
Titre : NHibernate in Action, Auteur : Pierre-Henri Kuaté, Editeur : Manning, ISBN-13 : 978-1932394924
Un ORM (Object Relational Mapper) est un ensemble de bibliothèques permettant à un programme exploitant une base de données d'exploiter celle-ci sans émettre d'ordres SQL explicites et sans connaître les particularités du SGBD utilisé.
Pré-requis
Dans une échelle [débutant-intermédiaire-avancé], ce document est dans la partie [intermédiaire]. Sa compréhension nécessite divers pré-requis qu'on pourra trouver dans certains des documents que j'ai écrits :
- Langage C# 2008 : [Apprentissage du langage C# Version 3.0 avec le Framework .NET 3.5 ]
- [Spring IoC], disponible à l'URL [Spring IoC pour .NET ]. Présente les bases de l'inversion de contrôle (Inversion of Control) ou injection de dépendances (Dependency Injection) du framework Spring.Net [Spring.NET | Homepage ].
Des conseils de lecture sont parfois donnés au début des paragraphes de ce document. Ils référencent les documents précédents.
Outils
Les outils utilisés dans cette étude de cas sont librement disponibles sur le web. Ce sont les suivants (décembre 2011) :
- Nhibernate 3.2 disponible à l'Url [http://nhforge.org/Default.aspx]
- Spring.net 1.3.2 disponible à l'Url [http://www.springframework.net]. Le framework Spring.net est très riche. Nous utiliserons ici que la bibliothèque qu'il amène pour faciliter l'utilisation du framwork Nhibernate.
- Log4net 1.2.10 disponible à l'Url [http://logging.apache.org/log4net]. Ce framework de logs est utilisé par Nhibernate.
- Nunit 2.5 disponible à l'Url [http://www.nunit.org/]. Ce framework de tests unitaires est l'équivalent pour .NET du framework JUnit pour la plate-forme Java.
- Le pilote ADO.NET 6.4.4 du SGBD MySQL 5 disponible à l'Url [http://dev.mysql.com/downloads/connector/net]
L'ensemble des DLL nécessaires aux projets Visual Studio 2010 ont été rassemblés dans un dossier [libnet4] :
![]() |
6.1. La place de NHIBERNATE dans une architecture .NET en couches
Une application .NET utilisant une base de données peut être architecturée en couches de la façon suivante :
![]() |
La couche [dao] communique avec le SGBD via l'API ADO.NET (voir paragraphe 3.3).Dans l'architecture précédente, le connecteur [ADO.NET] est lié au SGBD. Ainsi la classe implémentant l'interface [IDbConnection] est :
- la classe [MySQLConnection] pour le SGBD MySQL
- la classe [SQLConnection] pour le SGBD SQLServer
La couche [dao] est ainsi dépendante du SGBD utilisé. Certains frameworks (Linq, Ibatis.net, NHibernate) lèvent cette contrainte en ajoutant une couche supplémentaire entre la couche [dao] et le connecteur [ADO.NET] du SGBD utilisé. Nous utiliserons ici, le framework [NHibernate].
![]() |
Ci-dessus, la couche [dao] ne s'adresse plus au connecteur [ADO.NET] mais au framework NHibernate qui va lui présenter une interface indépendante du connecteur [ADO.NET] utilisé. Cette architecture permet de changer de SGBD sans changer la couche [dao]. Seul le connecteur [ADO.NET] doit être alors changé.
6.2. La base de données exemple
Pour montrer comment travailler avec NHibernate, nous utiliserons la base de données MySQL [dbpam_nhibernate] suivante décrite au paragraphe 3.1. L'exportation de la structure de la base vers un fichier SQL donne le résultat suivant :
On notera, lignes 6, 20 et 36 que les clés primaires ID ont l'attribut autoincrement. Ceci signifie que MySQL génèrera automatiquement les valeurs des clés primaires à chaque ajout d'un enregistrement. Le développeur n'a pas à s'en préoccuper.
6.3. Le projet C# de démonstration
Pour introduire la configuration et l'utilisation de NHibernate, nous utiliserons l'architecture suivante :
![]() |
Un programme console [1] manipulera les données de la base de données précédente [2] via le framework [NHibernate] [3]. Cela nous amènera à présenter :
- les fichiers de configuration de NHibernate
- l'API de NHibernate
Le projet C# sera le suivant :
![]() |
Les éléments nécessaires au projet sont les suivants :
- en [1], les DLL dont a besoin le projet :
- [NHibernate] : la DLL du framework NHibernate
- [MySql.Data] : la DLL du connecteur ADO.NET du SGBD MySQL
- [log4net] : la DLL du framework Log4net permettant de générer des logs
- en [2], les classes images des tables de la base de données
- en [3], le fichier [App.config] qui configure l'application tout entière, dont le framework [NHibernate]
- en [4], des applications console de test
6.3.1. Configuration de la connexion à la base de données
Revenons à l'architecture de test :
![]() |
Ci-dessus, [NHibernate] doit pouvoir accéder à la base de données. Pour cela, il a besoin de certaines informations :
- le SGBD qui gère la base (MySQL, SQLServer, Postgres, Oracle, ...). La plupart des SGBD ont ajouté au langage SQL des extensions qui leur sont propres. En connaissant le SGBD, NHibernate peut adapter les ordres SQL qu'il émet à ce SGBD. NHibernate utilise la notion de dialecte SQL.
- les paramètres de connexion à la base de données (nom de la base, nom de l'utilisateur propriétaire de la connexion, son mot de passe)
Ces informations peuvent être placées dans le fichier de configuration [App.config]. Voici celui qui sera utilisé avec une base MySQL 5 :
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- sections de configuration -->
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
</configSections>
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
<!-- This section contains the log4net configuration settings -->
<!-- NOTE IMPORTANTE : les logs ne sont pas actifs par défaut. Il faut les activer par programme avec l'instruction log4net.Config.XmlConfigurator.Configure();
! -->
<log4net>
<!-- Define an output appender (where the logs can go) -->
<appender name="LogFileAppender" type="log4net.Appender.FileAppender, log4net">
<param name="File" value="log.txt" />
<param name="AppendToFile" value="false" />
<layout type="log4net.Layout.PatternLayout, log4net">
<param name="ConversionPattern" value="%d [%t] %-5p %c [%x] <%X{auth}> - %m%n" />
</layout>
</appender>
<appender name="LogDebugAppender" type="log4net.Appender.DebugAppender, log4net">
<layout type="log4net.Layout.PatternLayout, log4net">
<param name="ConversionPattern" value="%d [%t] %-5p %c [%x] <%X{auth}> - %m%n"/>
</layout>
</appender>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender, log4net">
<layout type="log4net.Layout.PatternLayout, log4net">
<param name="ConversionPattern" value="%d [%t] %-5p %c [%x] <%X{auth}> - %m%n"/>
</layout>
</appender>
<!-- Setup the root category, set the default priority level and add the appender(s) (where the logs will go) -->
<root>
<priority value="INFO" />
<!--
<appender-ref ref="LogFileAppender" />
<appender-ref ref="LogDebugAppender"/>
-->
<appender-ref ref="ConsoleAppender"/>
</root>
<!-- Specify the level for some specific namespaces -->
<!-- Level can be : ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF -->
<logger name="NHibernate">
<level value="INFO" />
</logger>
</log4net>
</configuration>
- lignes 4-7 : définissent des sections de configuration dans le fichier [App.config]. Considérons la ligne 6 :
<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
Cette ligne définit la section de configuration de NHibernate dans le fichier [App.config]. Elle a deux attributs : name et type.
- l'attribut [name] nomme la section de configuration. Cette section doit être ici délimitée par les balises <name>...</name>, ici <hibernate-configuration>...</hibernate-configuration> des lignes 11-24.
- l'attribut [type=classe,DLL] indique le nom de la classe chargée de traiter la section définie par l'attribut [name] ainsi que la DLL contenant cette classe. Ici, la classe s'appelle [NHibernate.Cfg.ConfigurationSectionHandler] et se trouve dans la DLL [NHibernate.dll]. On se rappelle que cette DLL fait partie des références du projet étudié.
Considérons maintenant la section de configuration de NHibernate :
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
- ligne 2 : la configuration de NHibernate est à l'intérieur d'une balise <hibernate-configuration>. L'attribut xmlns (Xml NameSpace) fixe la version utilisée pour configurer NHibernate. En effet, au fil du temps, la façon de configurer NHibernate a évolué. Ici, c'est la version 2.2 qui est utilisée.
- ligne 3 : la configuration de NHibernate est ici tout entière contenue dans la balise <session-factory> (lignes 3 et 14). Une session NHibernate, est l'outil utilisé pour travailler avec une base de données selon le schéma :
- ouverture session
- travail avec la base de données via les méthodes de l'API NHibernate
- fermeture session
La session est créée par une factory, un terme générique désignant une classe capable de créer des objets. Les lignes 3-14 configurent cette factory.
- lignes 4, 6, 8, 9 : configurent la connexion à la base de données cible. Les principales informations sont le nom du SGBD utilisé, le nom de la base, l'identité de l'utilisateur et son mot de passe.
- ligne 4 : définit le fournisseur de la connexion, celui auprès duquel on demande une connexion vers la base de données. La valeur de la propriété [connection.provider] est le nom d'une classe NHibernate. Cette propriété ne dépend pas du SGBD utilisé.
- ligne 6 : le pilote ADO.NET à utiliser. C'est le nom d'une classe NHibernate spécialisée pour un SGBD donné, ici MySQL. La ligne 6 a été mise en commentaires, car elle n'est pas indispensable.
- ligne 8 : la propriété [dialect] fixe le dialecte SQL à utiliser avec le SGBD. Ici c'est le dialecte du SGBD MySQL.
Si on change de SGBD, comment trouve-t-on le dialecte NHibernate de celui-ci ? Revenons au projet C# précédent et double-cliquons sur la DLL [NHibernate] dans l'onglet [References] :
![]() |
- en [1], l'onglet [Explorateur d'objets] affiche un certain nombre de DLL, dont celles référencées par le projet.
- en [2], la DLL [NHibernate]
- en [3], la DLL [NHibernate] développée. On y trouve les différents espaces de noms (namespace) qui y sont définis.
- en [4], l'espace de noms [NHibernate.Dialect] où l'on trouve les classes définissant les différents dialectes SQL utilisables.
- en [5], la classe du dialecte du SGBD MySQL 5.
![]() |
- en [6], l'espace de noms de la classe [MySqlDataDriver] utilisé ligne 6 ci-dessous :
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQLDialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
- lignes 9-11 : la chaîne de connexion à la base de données. Cette chaîne est de la forme "param1=val1;param2=val2; ...". L'ensemble des paramètres ainsi définis permet au pilote du SGBD de créer une connexion. La forme de cette chaîne de connexion est dépendante du SGBD utilisé. On trouve les chaînes de connexion aux principaux SGBD sur le site [http://www.connectionstrings.com/]. Ici, la chaîne "Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;" est une chaîne de connexion pour le SGBD MySQL. Elle indique que :
- Server=localhost; : le SGBD est sur la même machine que le client qui cherche à ouvrir la connexion
- Database=dbpam_nhibernate; : la base de données MySQL visée
- Uid=root; : l'utilisateur qui ouvre la connexion est l'utilisateur root
- Pwd=; : cet utilisateur n'a pas de mot de passe (cas particulier de cet exemple)
- ligne 12 : la propriété [show_sql] indique si NHibernate doit afficher dans ses logs, les ordres SQL qu'il émet sur la base de données. En phase de développement, il est utile de mettre cette propriété à [true] pour savoir exactement ce que fait NHibernate.
- ligne 13 : pour comprendre la balise <mapping>, revenons à l'architecture de l'application :
![]() |
Si le programme console était un client direct du connecteur ADO.NET et qu'il voulait la liste des employés, il ferait exécuter au connecteur un ordre SQL Select, et il recevrait en retour un objet de type IDataReader qu'il aurait à traiter pour obtenir la liste des employés désirée initialement.
Ci-dessus, le programme console est le client de NHibernate et NHibernate est le client du connecteur ADO.NET. Nous verrons ultérieurement que l'API de NHibernate va permettre au programme console de demander la liste des employés. NHibernate va traduire cette demande en un ordre SQL Select qu'il va faire exécuter au connecteur ADO.NET. Celui-ci va lui rendre un objet de type IDataReader . A partir de cet objet, Nhibernate doit être capable de construire la liste des employés qui lui a été demandée. C'est par configuration que cela est rendu possible. A chaque table de la base de données est associé une classe C#. Ainsi à partir des lignes de la table [employes] renvoyées par le IDataReader, NHibernate va être capable de construire une liste d'objets représentant des employés et rendre celle-ci au programme console. Ces relations tables <--> classes sont créées dans des fichiers de configuration. NHibernate utilise le terme "mapping" pour définir ces relations.
Revenons à la ligne 13 ci-dessous :
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
La ligne 13 indique que les fichiers de configuration tables <--> classes seront trouvés dans l'assembly [pam-nhibernate-demos]. Un assembly est l'exécutable ou la DLL produit par la compilation d'un projet. Ici, les fichiers de mapping seront placés dans l'assembly du projet exemple. Pour connaître le nom de cet assembly, il faut regarder les propriétés du projet :
![]() |
- en [1], les propriétés du projet
- dans l'onglet [Application] [2], le nom de l'assembly [3] qui va être généré.
- parce que le type de sortie est [Application console] [4], le fichier généré à la compilation du projet s'appellera [pam-nhibernate-demos.exe]. Si le type de sortie était [Bibliothèque de classes] [5], le fichier généré à la compilation du projet s'appellerait [pam-nhibernate-demos.dll]
- l'assembly est généré dans le dossier [bin/Release] du projet [6].
On retiendra de l'explication précédente que les fichiers de mapping tables <--> classes devront être dans le fichier [pam-nhibernate-demos.exe] [6].
6.3.2. Configuration du mapping tables <-->classes
Revenons à l'architecture du projet étudié :
![]() |
- en [1] le programme console utilise les méthodes de l'API du framework NHibernate. Ces deux blocs échangent des objets.
- en [2], NHibernate utilise l'API d'un connecteur .NET. Il émet des ordres SQL vers le SGBD cible.
Le programme console va manipuler des objets reflétant les tables de la base de données. Dans ce projet, ces objets et les liens qui les unissent aux tables de la base de données ont été placés dans le dossier [Entites] ci-dessous :
![]() |
- chaque table de la base de données fait l'objet d'une classe et d'un fichier de mapping entre les deux
Table | Classe | Mapping |
cotisations | Cotisations.cs | Cotisations.hbm.xml |
employes | Employe.cs | Employe.hbm.xml |
indemnites | Indemnites.cs | Indemnites.hbm.xml |
6.3.2.1. Mapping de la table [cotisations]
Considérons la table [cotisations] :
![]() |
|
Une ligne de cette table peut être encapsulée dans un objet de type [Cotisations.cs] suivant :
namespace PamNHibernateDemos {
public class Cotisations {
// propriétés automatiques
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual double CsgRds { get; set; }
public virtual double Csgd { get; set; }
public virtual double Secu { get; set; }
public virtual double Retraite { get; set; }
// constructeurs
public Cotisations() {
}
// ToString
public override string ToString() {
return string.Format("[{0}|{1}|{2}|{3}]", CsgRds, Csgd, Secu, Retraite);
}
}
}
On a créé une propriété automatique pour chacune des colonnes de la table [cotisations]. Chacune de ces propriétés doit être déclarée virtuelle (virtual) car NHibernate est amené à dériver la classe et à redéfinir (override) ses propriétés. Celles-ci doivent donc être virtuelles.
On notera, ligne 1, que la classe appartient à l'espace de noms [PamNHibernateDemos].
Le fichier de mapping [Cotisations.hbm.xml] entre la table [cotisations] et la classe [Cotisations] est le suivant :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
<class name="Cotisations" table="COTISATIONS">
<id name="Id" column="ID" unsaved-value="0">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="CsgRds" column="CSGRDS"/>
<property name="Csgd" column="CSGD"/>
<property name="Retraite" column="RETRAITE"/>
<property name="Secu" column="SECU"/>
</class>
</hibernate-mapping>
- le fichier de mapping est un fichier Xml défini à l'intérieur de la balise <hibernate-mapping> (lignes 2 et 14)
- ligne 4 : la balise <class> fait le lien entre une table de la base de données et une classe. Ici, la table [COTISATIONS] (attribut table) et la classe [Cotisations] (attribut name). En .NET, une classe doit être définie par son nom complet (espace de noms inclus) et par l'assembly qui la contient. Ces deux informations sont données par la ligne 3. La première (namespace) peut être trouvée dans la définition de la classe. La seconde (assembly) est le nom de l'assembly du projet. Nous avons déjà indiqué comment trouver ce nom.
- lignes 5-7 : la balise <id> sert à définir le mapping de la clé primaire de la table [cotisations].
- ligne 5 : l'attribut name désigne le champ de la classe [Cotisations] qui va recevoir la clé primaire de la table [cotisations]. L'attribut column désigne la colonne de de la table [cotisations] qui sert de clé primaire. L'attribut unsaved-value sert à définir une clé primaire non encore générée. Cette valeur permet à NHibernate de savoir comment sauvegarder un objet [Cotisations] dans la table [cotisations]. Si cet objet à un champ Id=0, il fera une opération SQL INSERT, sinon il fera une opération SQL UPDATE. La valeur de unsaved-value dépend du type du champ Id de la classe [Cotisations]. Ici, il est de type int et la valeur par défaut d'un type int est 0. Un objet [Cotisations] encore non sauvegardé (sans clé primaire donc) aura donc son champ Id=0. Si le champ Id avait été de type Object ou dérivé, on aurait écrit unsaved-value=null.
- ligne 6 : lorsque NHibernate doit sauvegarder un objet [Cotisations] avec un champ Id=0, il doit faire sur la base de données une opération INSERT au cours de laquelle il doit obtenir une valeur pour la clé primaire de l'enregistrement. La plupart des SGBD ont une méthode propriétaire pour générer automatiquement cette valeur. La balise <generator> sert à définir le mécanisme à utiliser pour la génération de la clé primaire. La balise <generator class="native"> indique qu'il faut utiliser le mécanisme par défaut du SGBD utilisé. Nous avons vu paragraphe 6.2 que les clés primaires des nos trois tables MySQL avaient l'attribut autoincrement. Lors de ses opérations INSERT, NHibernate ne fournira pas de valeur à la colonne ID de l'enregistrement ajouté, laissant MySQL générer cette valeur.
- ligne 8 : la balise <version> sert à définir la colonne de la table (ainsi que le champ de la classe qui va avec) qui permet de "versionner" les enregistrements. Au départ, la version vaut 1. Elle est incrémentée à chaque opération UPDATE. D'autre part, toute opération UPDATE ou DELETE est faite avec un filtre WHERE ID= id AND VERSION=v1. Un utilisateur ne peut donc modifier ou détruire un objet que s'il a la bonne version de celui-ci. Si ce n'est pas le cas, une exception est remontée par NHibernate.
- ligne 9 : la balise <property> sert à définir un mapping de colonne normale (ni clé primaire, ni colonne de version). Ainsi la ligne 9 indique que la colonne CSGRDS de la table [COTISATIONS] est associée à la propriété CsgRds de la classe [Cotisations].
6.3.2.2. Mapping de la table [indemnites]
Considérons la table [indemnites] :
![]() |
|
Une ligne de cette table peut être encapsulée dans un objet de type [Indemnites] suivant :
namespace PamNHibernateDemos {
public class Indemnites {
// propriétés automatiques
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual int Indice { get; set; }
public virtual double BaseHeure { get; set; }
public virtual double EntretienJour { get; set; }
public virtual double RepasJour { get; set; }
public virtual double IndemnitesCp { get; set; }
// constructeurs
public Indemnites() {
}
// identité
public override string ToString() {
return string.Format("[{0}|{1}|{2}|{3}|{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
}
}
}
Le fichier de mapping table [indemnites] <--> classe [Indemnites] pourrait être le suivant (Indemnites.hbm.xml) :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
<class name="Indemnites" table="INDEMNITES">
<id name="Id" column="ID" unsaved-value="0">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="Indice" column="INDICE" unique="true"/>
<property name="BaseHeure" column="BASE_HEURE" />
<property name="EntretienJour" column="ENTRETIEN_JOUR" />
<property name="RepasJour" column="REPAS_JOUR" />
<property name="IndemnitesCp" column="INDEMNITES_CP" />
</class>
</hibernate-mapping>
On ne trouve là rien de neuf vis à vis du fichier de mapping expliqué précédemment. La seule différence se trouve ligne 9. L'attribut unique="true" indique qu'il y a dans la table [indemnites] une contrainte d'unicité sur la colonne [INDICE] : il ne peut pas y avoir deux lignes avec la même valeur pour la colonne [INDICE].
6.3.2.3. Mapping de la table [employes]
Considérons la table [employes] :
![]() |
|
La nouveauté vis à vis des tables précédentes est la présence d'une clé étrangère : la colonne [INDEMNITE_ID] est une clé étrangère sur la colonne [ID] de la table [INDEMNITES]. Ce champ référence la ligne de la table [INDEMNITES] à utiliser pour calculer les indemnites de l'employé.
La classe [Employe] image de la table [employes] pourrait être la suivante :
namespace PamNHibernateDemos {
public class Employe {
// propriétés automatiques
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual string SS { get; set; }
public virtual string Nom { get; set; }
public virtual string Prenom { get; set; }
public virtual string Adresse { get; set; }
public virtual string Ville { get; set; }
public virtual string CodePostal { get; set; }
public virtual Indemnites Indemnites { get; set; }
// constructeurs
public Employe() {
}
// ToString
public override string ToString() {
return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
}
}
}
Le fichier de mapping [Employe.hbm.xml] pourrait être le suivant :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
<class name="Employe" table="EMPLOYES">
<id name="Id" column="ID" unsaved-value="0">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="SS" column="SS"/>
<property name="Nom" column="NOM"/>
<property name="Prenom" column="PRENOM"/>
<property name="Adresse" column="ADRESSE"/>
<property name="Ville" column="VILLE"/>
<property name="CodePostal" column="CP"/>
<many-to-one name="Indemnites" column="INDEMNITE_ID" cascade="save-update" lazy="false"/>
</class>
</hibernate-mapping>
La nouveauté réside ligne 15 avec l'apparition d'une nouvelle balise : <many-to-one>. Cette balise sert à mapper une colonne clé étrangère [INDEMNITE_ID] de la table [EMPLOYES] vers la propriété [Indemnites] de la classe [Employe] :
namespace PamNHibernateDemos {
public class Employe {
// propriétés automatiques
..
public virtual Indemnites Indemnites { get; set; }
...
}
}
La table [EMPLOYES] a une clé étrangère [INDEMNITE_ID] qui référence la colonne [ID] de la table [INDEMNITES]. Plusieurs (many) lignes de la table [EMPLOYES] peuvent référencer une même ligne (one) de la table [INDEMNITES]. D'où le nom de la balise <many-to-one>. Cette balise a ici les attributs suivants :
- column : indique le nom de la colonne de la table [EMPLOYES] qui est clé étrangère sur la table [INDEMNITES]
- name : indique la propriété de la classe [Employe] associée à cette colonne. Le type de cette propriété est nécessairement la classe associée à la table cible de la clé étrangère, ici la table [INDEMNITES]. On sait que cette classe est la classe [Indemnites] déjà décrite. C'est ce que reflète la ligne 5 ci-dessus. Cela signifie que lorsque NHibernate ramènera de la base un objet [Employe], il ramènera également l'objet [Indemnites] qui va avec.
- cascade : cet attribut peut avoir diverses valeurs :
- save-update : une opération d'insertion (save) ou de mise à jour (update) sur l'objet [Employe] doit être propagée sur l'objet [Indemnites] qu'il contient.
- delete : la suppression d'un objet [Employe] doit être propagée à l'objet [Indemnites] qu'il contient.
- all : propage les opérations d'insertion (save), de mise à jour (update) et de suppression (delete).
- none : ne propage rien
Pour terminer, rappelons la configuration de NHibernate dans le fichier [App.config] :
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
La ligne 13 indique que les fichiers de mapping *.hbm.xml seront trouvés dans l'assembly [pam-nhibernate-demos]. Ceci n'est pas fait par défaut. Il faut le configurer dans le projet C# :
![]() |
- en [1], on sélectionne les propriétés d'un fichier de mapping
- en [2], l'action de génération doit être [Ressource incorporée] [3]. Cela signifie qu'à la génération du projet, le fichier de mapping doit être incorporé dans l'assembly généré.
6.4. l'API de NHibernate
Revenons à l'architecture de notre projet exemple :
![]() |
Dans les paragraphes précédents, nous avons configuré NHibernate de deux façons :
- dans [App.config], nous avons configuré la connexion à la base de données
- nous avons écrit pour chaque table de la base, la classe image de cette table et le fichier de mapping qui permet de passer de la classe à la table et vice-versa.
Il nous reste à découvrir les méthodes offertes par NHibernate pour manipuler les données de la base : insertion, mise à jour, suppression, liste.
6.4.1. L'objet SessionFactory
Toute opération NHibernate se fait à l'intérieur d'une session. Une séquence typique d'opérations NHibernate est la suivante :
- ouvrir une session NHibernate
- commencer une transaction dans la session
- faire des opérations de persistance avec la session (Load, Get, Find, CreateQuery, Save, SaveOrUpdate, Delete)
- valider (commit) ou invalider (rollback) la transaction
- fermer la session NHibernate
Une session est obtenue auprès d'une factory de type [SessionFactory]. Cette factory est celle configurée par la balise <session-factory> dans le fichier de configuration [App.config] :
<!-- configuration NHibernate -->
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<!--
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
-->
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
<property name="connection.connection_string">
Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
</property>
<property name="show_sql">false</property>
<mapping assembly="pam-nhibernate-demos"/>
</session-factory>
</hibernate-configuration>
Dans un code C#, la SessionFactory peut être obtenue de la façon suivante :
ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();
La classe Configuration est une classe du framework NHibernate. L'instruction précédente exploite la section de configuration de NHibernate dans [App.config]. L'objet [ISessionFactory] obtenu a alors les :
- informations pour créer une connexion à la base de données cible
- fichiers de mapping entre tables de la base de données et classes persistantes manipulées par NHibernate.
6.4.2. La session NHibernate
Une fois la SessionFactory créée (cela se fait une unique fois), on peut en obtenir les sessions permettant de faire des opérations de persistance NHibernate. Un code usuel est le suivant :
try{
// ouverture session
using (ISession session = sessionFactory.OpenSession())
{
// début transaction
using (ITransaction transaction = session.BeginTransaction())
{
........................ opérations de persistance
// validation de la transaction
transaction.Commit();
}
}
}catch (Exception ex){
....
}
- ligne 3 : une session est créée à partir de la SessionFactory à l'intérieur d'une clause using. A la sortie de la clause using, la session sera automatiquement fermée. Sans la clause using, il faudrait fermer la session explicitement (session.Close()).
- ligne 6 : les opérations de persistance vont se faire à l'intérieur d'une transaction. Soit elles réussissent toutes, soit aucune ne réussit. A l'intérieur de la clause using, la transaction est validée par un Commit (ligne 10). Si dans la transaction, une opération de persistance lance une exception, la transaction sera automatiquement invalidée par un Rollback à la sortie du using.
- le try / catch des lignes 1 et 13 permet d'intercepter une éventuelle exception lancée par le code à l'intérieur du try (session, transaction, persistance).
6.4.3. L'interface ISession
Nous présentons maintenant certaines des méthodes de l'interface ISession implémentée par une session NHibernate :
démarre une transaction dans la session ITransaction tx=session.BeginTransaction(); | |
vide la session. Les objets qu'elle contenait deviennent détachés. session.Clear(); | |
ferme la session. Les objets qu'elle contenait sont synchronisés avec la base de données. Cette opération de synchronisation est également faite à la fin d'une transaction. Ce dernier cas est le plus courant. session.Close(); | |
crée une requête HQL (Hibernate Query Language) pour une exécution ultérieure. IQuery query=session.createQuery("select e from Employe e); | |
supprime un objet. Celui-ci peut appartenir à la session (attaché) ou non (détaché). Lors de la synchronisation de la session avec la base de données, une opération SQL DELETE sera faite sur cet objet. // on charge un employé de la BD Employe e=session.Get<Employe>(143); // on le supprime session.Delete(e); | |
force la synchronisation de la session avec la base de données. Le contenu de la session ne change pas. session.Flush(); | |
va chercher dans la base l'objet T de clé primaire id. Si cet objet n'existe pas, rend le pointeur null. // on charge un employé de la BD Employe e=session.Get<Employe>(143); | |
met l'objet obj dans la session. Cet objet n'a pas de clé primaire avant le Save. Après le Save, il en a une. Lors de la synchronisation de la session, une opération SQL INSERT sera faite sur la base. // on crée un employé Employe e=new Employe(){...}; // on le sauvegarde e=session.Save(e); | |
fait une opération Save si obj n'a pas de clé primaire ou une opération Update s'il en a déjà une. | |
met à jour dans la base de données, l'objet obj. Une opération SQL UPDATE est alors faite sur la base. // on charge un employé de la BD Employe e=session.Get<Employe>(143); // on change son nom e.Nom=...; // on le met à jour dans la base session.Update(e); |
6.4.4. L'interface IQuery
L'interface IQuery permet de requêter la base de données pour en extraire des données. Nous avons vu comment en créer une instance :
Le paramètre de la méthode createQuery est une requête HQL (Hibernate Query Language), un langage analogue au langage SQL mais requêtant des classes plutôt que des tables. La requête ci-dessus demande la liste de tous les employés. Voici quelques exemples de requêtes HQL :
select e from Employe e where e.Nom like 'A%'
select e from Employe order by e.Nom asc
select e from Employe e where e.Indemnites.Indice=2
Nous présentons maintenant certaines des méthodes de l'interface IQuery :
rend le résultat de la requête sous la forme d'une liste d'objets T IList<Employe> employes=session.createQuery("select e from Employe e order by e.Nom asc").List<Employe>(); | |
rend le résultat de la requête sous la forme d'une liste où chaque élément de la liste représente une ligne résultat du Select sous la forme d'un tableau d'objets. IList lignes=session.createQuery("select e.Nom, e.Prenom, e.SS from Employe e").List(); lignes[i][j] représente la colonne j de la ligne i dans un type object. Ainsi lignes[10][1] est un type object représentant le prénom d'une personne. Des transtypages sont en général nécessaires pour récupérer les données dans leur type exact. | |
rend le premier objet du résultat de la requête Employe e=session.createQuery("select e from Employe e where e.Nom='MARTIN'").UniqueResult<Employe>(); |
Une requête HQL peut être paramétrée :
Dans la requête HQL de la ligne 3, :num est un paramètre qui doit recevoir une valeur avant que la requête ne soit exécutée. Ci-dessus, c'est la méthode SetString qui est utilisée pour cela. L'interface IQuery dispose de diverses méthodes Set pour affecter une valeur à un paramètre :
- - SetBoolean(string name, bool value)
- - SetSingle(string name, single value)
- - SetDouble(string name, double value)
- - SetInt32(string name, int32 value)
- ..
6.5. Quelques exemples de code
Les exemples qui suivent s'appuient sur l'architecture étudiée précédemment et rappelée ci-dessous. La base de données est la base de données MySQL [dbpam_nhibernate] également présentée. Les exemples sont des programmes console [1] utilisant le framework NHibernate [3] pour manipuler la base de données [2].
![]() |
Le projet C# dans lequel s'insèrent les exemples qui vont suivre est celui déjà présenté :
![]() |
- en [1], les DLL dont a besoin le projet :
- [NHibernate] : la DLL du framework NHibernate
- [MySql.Data] : la DLL du connecteur ADO.NET du SGBD MySQL 5
- [log4net] : la DLL d'un outil permettant de générer des logs
- en [2], les classes images des tables de la base de données
- en [3], le fichier [App.config] qui configure l'application tout entière, dont le framework [NHibernate]
- en [4], des applications console de test. Ce sont celles-ci que nous allons présenter partiellement.
6.5.1. Obtenir le contenu de la base
Le programme [ShowDataBase.cs] permet d'afficher le contenu de la base :
using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;
namespace PamNHibernateDemos
{
public class ShowDataBase
{
private static ISessionFactory sessionFactory = null;
// programme principal
static void Main(string[] args)
{
// initialisation factory NHibernate
sessionFactory = new Configuration().Configure().BuildSessionFactory();
try
{
// affichage contenu de la base
Console.WriteLine("Affichage base -------------------------------------");
ShowDataBase1();
}
catch (Exception ex)
{
// on affiche l'exception
Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
}
finally
{
if (sessionFactory != null)
{
sessionFactory.Close();
}
}
// attente clavier
Console.ReadLine();
}
// test1
static void ShowDataBase1()
{
// ouverture session
using (ISession session = sessionFactory.OpenSession())
{
// début transaction
using (ITransaction transaction = session.BeginTransaction())
{
// on récupère la liste des employés
IList<Employe> employes = session.CreateQuery(@"select e from Employe e order by e.Nom asc").List<Employe>();
// on l'affiche
Console.WriteLine("--------------- liste des employés");
foreach (Employe e in employes)
{
Console.WriteLine(e);
}
// on récupère la liste des indemnites
IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i order by i.Indice asc").List<Indemnites>();
// on l'affiche
Console.WriteLine("--------------- liste des indemnités");
foreach (Indemnites i in indemnites)
{
Console.WriteLine(i);
}
// on récupère la liste des cotisations
Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
Console.WriteLine("--------------- tableau des taux de cotisations");
Console.WriteLine(cotisations);
// commit transaction
transaction.Commit();
}
}
}
}
}
Explications :
- ligne 19 : l'objet SessionFactory est créé. C'est lui qui va nous permettre d'obtenir des objets Session.
- ligne 24 : on affiche le contenu de la base
- lignes 31-37 : la SessionFactory est fermée dans la clause finally du try.
- ligne 43 : la méthode qui affiche le contenu de la base
- ligne 46 : on obtient une Session auprès de la SessionFactory.
- ligne 49 : on démarre une transaction
- ligne 52 : requête HQL pour récupérer la liste des employés. A cause de la clé étrangère qui lie l'entité Employe à l'entité Indemnite, avec chaque emloyé, on aura son indemnité.
- ligne 60 : requête HQL pour obtenir la liste des indemnités.
- ligne 68 : requête HQL pour obtenir l'unique ligne de la table des cotisations.
- ligne 72 : fin de la transaction
- ligne 73 : fin du using Itransaction de la ligne 49 – la transaction est automatiquement fermée
- ligne 74 : fin du using Isession de la ligne 46 – la session est automatiquement fermée.
Affichage écran obtenu :
On notera lignes 3 et 4 qu'en demandant un employé, on a également obtenu son indemnité.
6.5.2. Insérer des données dans la base
Le programme [FillDataBase.cs] permet d'insérer des données dans la base :
using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;
namespace PamNHibernateDemos
{
public class FillDataBase
{
private static ISessionFactory sessionFactory = null;
// programme principal
static void Main(string[] args)
{
// initialisation factory NHibernate
sessionFactory = new Configuration().Configure().BuildSessionFactory();
try
{
// suppression du contenu de la base
Console.WriteLine("Effacement base -------------------------------------");
ClearDataBase1();
Console.WriteLine("Affichage base -------------------------------------");
ShowDataBase();
Console.WriteLine("Remplissage base -------------------------------------");
FillDataBase1();
Console.WriteLine("Affichage base -------------------------------------");
ShowDataBase();
}
catch (Exception ex)
{
// on affiche l'exception
Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
}
finally
{
if (sessionFactory != null)
{
sessionFactory.Close();
}
}
// attente clavier
Console.ReadLine();
}
// test1
static void ShowDataBase()
{
// voir exemple précédent
}
// ClearDataBase1
static void ClearDataBase1()
{
// ouverture session
using (ISession session = sessionFactory.OpenSession())
{
// début transaction
using (ITransaction transaction = session.BeginTransaction())
{
// on récupère la liste des employés
IList<Employe> employes = session.CreateQuery(@"select e from Employe e").List<Employe>();
// on supprime tous les employés
Console.WriteLine("--------------- suppression des employés associés");
foreach (Employe e in employes)
{
session.Delete(e);
}
// on récupère la liste des indemnités
IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i").List<Indemnites>();
// on supprime les indemnités
Console.WriteLine("--------------- suppression des indemnités");
foreach (Indemnites i in indemnites)
{
session.Delete(i);
}
// on récupère la liste des cotisations
Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
Console.WriteLine("--------------- suppression des taux de cotisations");
if (cotisations != null)
{
session.Delete(cotisations);
}
// commit transaction
transaction.Commit();
}
}
}
// FillDataBase
static void FillDataBase1()
{
// ouverture session
using (ISession session = sessionFactory.OpenSession())
{
// début transaction
using (ITransaction transaction = session.BeginTransaction())
{
// on crée deux indemnités
Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
Indemnites i2 = new Indemnites() { Id = 0, Indice = 2, BaseHeure = 2.1, EntretienJour = 2.1, RepasJour = 3.1, IndemnitesCp = 15 };
// on crée deux employés
Employe e1 = new Employe() { Id = 0, SS = "254104940426058", Nom = "Jouveinal", Prenom = "Marie", Adresse = "5 rue des oiseaux", Ville = "St Corentin", CodePostal = "49203", Indemnites = i1 };
Employe e2 = new Employe() { Id = 0, SS = "260124402111742", Nom = "Laverti", Prenom = "Justine", Adresse = "La Brûlerie", Ville = "St Marcel", CodePostal = "49014", Indemnites = i2 };
// on crée les taux de cotisations
Cotisations cotisations = new Cotisations() { Id = 0, CsgRds = 3.49, Csgd = 6.15, Secu = 9.39, Retraite = 7.88 };
// on sauvegarde le tout
session.Save(e1);
session.Save(e2);
session.Save(cotisations);
// commit transaction
transaction.Commit();
}
}
}
}
}
Explications
- ligne 19 : la SessionFactory est créée
- lignes 37-43 : elle est fermée dans la clause finally du try
- ligne 55 : la méthode ClearDataBase1 qui vide la base de données. Le principe est le suivant :
- on récupère tous les employés (ligne 64) dans une liste
- on les supprime un à un (lignes 67-70)
- ligne 93 : la méthode FillDataBase1 insère quelques données dans la base de données
- on crée deux entités Indemnites (lignes 102, 103)
- on crée deux employés ayant ces indemnités (lignes 105, 106)
- on crée un objet Cotisations en ligne 108.
- lignes 110, 111 : les deux entités Employe sont persistées dans la base de données
- ligne 112 : l'entité Cotisations est persistée à son tour
- on peut s'étonner que les entités Indemnités des lignes 102 et 103 n'aient pas été persistées. En fait elle l'ont été en même temps que les entités Employe. Pour le comprendre, il fait revenir au mapping de l'entité Employe :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="PamNHibernateDemos" assembly="pam-nhibernate-demos">
<class name="Employe" table="EMPLOYES">
<id name="Id" column="ID" unsaved-value="0">
<generator class="native" />
</id>
<version name="Version" column="VERSION"/>
<property name="SS" column="SS"/>
<property name="Nom" column="NOM"/>
<property name="Prenom" column="PRENOM"/>
<property name="Adresse" column="ADRESSE"/>
<property name="Ville" column="VILLE"/>
<property name="CodePostal" column="CP"/>
<many-to-one name="Indemnites" column="INDEMNITE_ID" cascade="save-update" lazy="false"/>
</class>
</hibernate-mapping>
La ligne 15 qui mappe la relation de clé étrangère qui l'entité Employe à l'entité Indemnites a l'attribut cascade= "save-update ", ce qui entraine que les opérations " save " et " update " de l'entité Employe sont propagées à l'entité Indemnites interne.
Affichage écran obtenu :
Effacement base -------------------------------------
--------------- suppression des employés et des indemnités associées
--------------- suppression des indemnités restantes
--------------- suppression des taux de cotisations
Affichage base -------------------------------------
--------------- liste des employés
--------------- liste des indemnités
--------------- tableau des taux de cotisations
Remplissage base -------------------------------------
Affichage base -------------------------------------
--------------- liste des employés
[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[2|2,1|2,1|3,1|15]]
[260124402111742|Laverti|Justine|La Brûlerie|St Marcel|49014|[1|1,93|2|3|12]]
--------------- liste des indemnités
[1|1,93|2|3|12]
[2|2,1|2,1|3,1|15]
--------------- tableau des taux de cotisations
[3,49|6,15|9,39|7,88]
6.5.3. Recherche d'un employé
Le programme [Program.cs] a diverses méthodes illustrant l'accès et la manipulation des données de la base. Nous en présentons quelques-unes.
La méthode [FindEmployee] permet de trouver un employé d'après son n° de sécurité sociale :
// FindEmployee
static void FindEmployee() {
try {
// ouverture session
using (ISession session = sessionFactory.OpenSession()) {
// début transaction
using (ITransaction transaction = session.BeginTransaction()) {
// on recherche un employé à partir de son n° SS
String numSecu = "254104940426058";
IQuery query = session.CreateQuery(@"select e from Employe e where e.SS=:numSecu");
Employe employe = query.SetString("numSecu", numSecu).UniqueResult<Employe>();
if (employe != null) {
Console.WriteLine("Employe[" + numSecu + "]=" + employe);
} else {
Console.WriteLine("Employe[" + numSecu + "] non trouvé...");
}
numSecu = "xx";
employe = query.SetString("numSecu", numSecu).UniqueResult<Employe>();
if (employe != null) {
Console.WriteLine("Employe[" + numSecu + "]=" + employe);
} else {
Console.WriteLine("Employe[" + numSecu + "] non trouvé...");
}
// commit transaction
transaction.Commit();
}
}
} catch (Exception e) {
Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
}
}
Explications
- ligne 10 : la requête Select paramétrée par numSecu à exécuter
- ligne 11 : l'affectation d'une valeur au paramètre numSecu et l'exécution de la méthode UniqueResult pour avoir un seul résultat.
Affichage écran obtenu :
Recherche d'un employé -------------------------------------
Employe[254104940426058]=[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[2|2,1|2,1|3,1|15]]
Employe[xx] non trouvé...
6.5.4. Insertion d'entités invalides
La méthode suivante tente de sauvegarder une entité [Employe] non initialisée.
// SaveEmptyEmployee
static void SaveEmptyEmployee() {
try {
// ouverture session
using (ISession session = sessionFactory.OpenSession()) {
// début transaction
using (ITransaction transaction = session.BeginTransaction()) {
// on crée un employe vide
Employe e = new Employe();
// on crée une indemnité non existante
Indemnites i = new Indemnites() { Id = 0, Indice = 3, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
// qu'on associe à l'employé
e.Indemnites = i;
// on sauvegarde l'employé en laissant vides les autres champs
session.Save(e);
// commit transaction
transaction.Commit();
}
}
} catch (Exception e) {
Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
}
}
Explications
Rappelons le code de la classe [Employe] :
namespace PamNHibernateDemos {
public class Employe {
// propriétés automatiques
public virtual int Id { get; set; }
public virtual int Version { get; set; }
public virtual string SS { get; set; }
public virtual string Nom { get; set; }
public virtual string Prenom { get; set; }
public virtual string Adresse { get; set; }
public virtual string Ville { get; set; }
public virtual string CodePostal { get; set; }
public virtual Indemnites Indemnites { get; set; }
// constructeurs
public Employe() {
}
// ToString
public override string ToString() {
return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
}
}
}
Un objet [Employe] non initialisé, aura la valeur null pour tous ses champs de type string. Lors de l'insertion de l'enregistrement dans la table [employes], NHibernate laissera vides les colonnes correspondant à ces champs. Or dans la table [employes], toutes les colonnes ont l'attribut not null, ce qui interdit les colonnes sans valeur. Le pilote ADO.NET lancera alors une exception :
6.5.5. Création de deux indemnités de même indice à l'intérieur d'une transaction
Dans la table [indemnites], la colonne [indice] a été déclarée avec l'attribut unique, ce qui interdit d'avoir deux lignes avec le même indice. La méthode suivante crée deux indemnités de même indice à l'intérieur d'une transaction :
// CreateIndemnites1
static void CreateIndemnites1() {
try {
// ouverture session
using (ISession session = sessionFactory.OpenSession()) {
// début transaction
using (ITransaction transaction = session.BeginTransaction()) {
// on crée deux indemnités de même indice
Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
Indemnites i2 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
// on les sauvegarde
session.Save(i1);
session.Save(i2);
// commit transaction
transaction.Commit();
}
}
} catch (Exception e) {
Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
}
}
Explications
- lignes 9 et 10, on crée deux entités Indemnites ayant le même indice. Or dans la base de données, la colonne INDICE a l'attribut UNIQUE.
- les lignes 12 et 13 mettent les deux entités Indemnites dans le contexte de persistance. Celui-ci est synchronisé avec la base de données lors de la validation de la transaction ligne 15. Cette synchronisation va provoquer deux INSERT. Le deuxième va provoquer une exception à cause de l'unicité de la colonne INDICE. Parce qu'on est à l'intérieur d'une transaction, le premier INSERT va être défait.
Le résultat obtenu est le suivant :
Ligne 9, on peut voir que la table [indemnites] est vide. Aucune insertion n'a eu lieu.
6.5.6. Création de deux indemnités de même indice hors transaction
La méthode suivante crée deux indemnités de même indice sans utiliser de transaction :
// CreateIndemnites2
static void CreateIndemnites2() {
try {
// ouverture session
using (ISession session = sessionFactory.OpenSession()) {
// on crée deux indemnités de même indice
Indemnites i1 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
Indemnites i2 = new Indemnites() { Id = 0, Indice = 1, BaseHeure = 1.94, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
// on les sauvegarde
session.Save(i1);
session.Save(i2);
}
} catch (Exception e) {
Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
}
}
Explications
- on a le même code que précédemment mais sans transaction.
- la synchronisation du contexte de persistance avec la base de données sera fait à la fermeture de ce contexte, ligne 13 (fermeture de la Session). La synchronisation va provoquer deux INSERT. Le deuxième va échouer à cause de l'unicité de la colonne INDICE. Mais comme on n'est pas dans une transaction, le premier INSERT ne sera pas défait.
Le résultat obtenu est le suivant :
La base était vide avant l'exécution de la méthode. Ligne 6, on peut voir que la table [indemnites] a une ligne.


















