Skip to content

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 :

  1. Langage C# 2008 : [Apprentissage du langage C# Version 3.0 avec le Framework .NET 3.5 ]
  2. [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 :

#
# Structure for the `cotisations` table : 
#

CREATE TABLE `cotisations` (
  `ID` bigint(20) NOT NULL auto_increment,
  `SECU` double NOT NULL,
  `RETRAITE` double NOT NULL,
  `CSGD` double NOT NULL,
  `CSGRDS` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1;

#
# Structure for the `indemnites` table : 
#

CREATE TABLE `indemnites` (
  `ID` bigint(20) NOT NULL auto_increment,
  `ENTRETIEN_JOUR` double NOT NULL,
  `REPAS_JOUR` double NOT NULL,
  `INDICE` int(11) NOT NULL,
  `INDEMNITES_CP` double NOT NULL,
  `BASE_HEURE` double NOT NULL,
  `VERSION` int(11) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `INDICE` (`INDICE`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;

#
# Structure for the `employes` table : 
#

CREATE TABLE `employes` (
  `ID` bigint(20) NOT NULL auto_increment,
  `PRENOM` varchar(20) NOT NULL,
  `SS` varchar(15) NOT NULL,
  `ADRESSE` varchar(50) NOT NULL,
  `CP` varchar(5) NOT NULL,
  `VILLE` varchar(30) NOT NULL,
  `NOM` varchar(30) NOT NULL,
  `VERSION` int(11) NOT NULL,
  `INDEMNITE_ID` bigint(20) NOT NULL,
  PRIMARY KEY  (`ID`),
  UNIQUE KEY `SS` (`SS`),
  KEY `FK_EMPLOYES_INDEMNITE_ID` (`INDEMNITE_ID`),
  CONSTRAINT `FK_EMPLOYES_INDEMNITE_ID` FOREIGN KEY (`INDEMNITE_ID`) REFERENCES `indemnites` (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;

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] &lt;%X{auth}&gt; - %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] &lt;%X{auth}&gt; - %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] &lt;%X{auth}&gt; - %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] :

ID
clé primaire de type autoincrement
VERSION
n° de version de l'enregistrement
SECU
taux (pourcentage) de cotisation pour la sécurité sociale
RETRAITE
taux de cotisation pour la retraite
CSGD
taux de cotisation pour la contribution sociale généralisée déductible
CSGRDS
taux de cotisation pour la contribution sociale généralisée et la contribution au remboursement de la dette sociale

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] :

ID
clé primaire de type autoincrement
VERSION
n° de version de l'enregistrement
BASE_HEURE
coût en euro d'une heure de garde
ENTRETIEN_JOUR
indemnité en euro par jour de garde
REPAS_JOUR
indemnité de repas en euro par jour de garde
INDEMNITES_CP
indemnités de congés payés. C'est un pourcentage à appliquer au salaire de base.

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] :

ID
clé primaire de type autoincrement
VERSION
n° de version de l'enregistrement
PRENOM
prénom de l'employé
NOM
son nom
ADRESSE
son adresse
CP
son code postal
VILLE
sa ville
INDEMNITE_ID
clé étrangère sur INDEMNITES(ID)

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 :

ITransaction BeginTransaction()
démarre une transaction dans la session
ITransaction tx=session.BeginTransaction();
void Clear()
vide la session. Les objets qu'elle contenait deviennent détachés.
session.Clear();
void Close()
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();
IQuery CreateQuery(string queryString)
crée une requête HQL (Hibernate Query Language) pour une exécution ultérieure.
IQuery query=session.createQuery("select e from Employe e);
void Delete(object obj)
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);
void Flush()
force la synchronisation de la session avec la base de données. Le contenu de la session ne change pas.
session.Flush();
T Get<T>(object id)
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);
object Save(object obj)
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);
SaveOrUpdate(object obj)
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.
void Update(object obj)
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 :

IQuery query=session.createQuery("select e from Employe e);

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 :

IList<T> List<T>()
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>();
IList List()
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.
T UniqueResult<T>()
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 :

1
2
3
string numSecu;
...
Employe e=session.createQuery("select e from Employe e where e.SS=:num").SetString("num",numSecu).UniqueResult<Employe>();

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 :

Affichage base -------------------------------------
--------------- liste des employés
[254104940426058|Jouveinal|Marie|5 rue des oiseaux|St Corentin|49203|[1|1,93|2|3|12]]
[260124402111742|Laverti|Justine|La Brûlerie|St Marcel|49014|[2|2,1|2,1|3,1|15]]

--------------- 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]

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 :

sauvegarde d'un employé vide -------------------------------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Employe][SQL: INSERT INTO EMPLOYES (VERSION, SS, NOM, PRENOM, ADRESSE, VILLE, CP, INDEMNITE_ID) VALUES (?, ?, ?, ?, ?, ?, ?, ?)]

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 :

Effacement base -------------------------------------
--------------- suppression des employés
--------------- suppression des indemnités
--------------- suppression des taux de cotisations
Création de deux indemnités de même indice dans une transaction --------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Indemnites][SQL: INSERT INTO INDEMNITES (VERSION, INDICE, BASE_HEURE, ENTRETIEN_JOUR, REPAS_JOUR, INDEMNITES_CP) VALUES (?, ?, ?, ?, ?, ?)]
Affichage base -------------------------------------
--------------- liste des employés
--------------- liste des indemnités
--------------- tableau des taux de cotisations

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 :

1
2
3
4
5
6
7
Création de deux indemnités de même indice sans transaction --------------
L'exception suivante s'est produite : could not insert: [PamNHibernateDemos.Indemnites][SQL: INSERT INTO INDEMNITES (VERSION, INDICE, BASE_HEURE, ENTRETIEN_JOUR, REPAS_JOUR, INDEMNITES_CP) VALUES (?, ?, ?, ?, ?, ?)]
Affichage base -------------------------------------
--------------- liste des employés
--------------- liste des indemnités
[1|1,93|2|3|12]
--------------- tableau des taux de cotisations

La base était vide avant l'exécution de la méthode. Ligne 6, on peut voir que la table [indemnites] a une ligne.