VII. Introduction à l'ORM NHIBERNATE▲
Ce document 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é.
Le document est illustré par une solution Visual Studio 2010 qu'on trouvera à l'Url [http://tahe.developpez.com/dotnet/nhibernate].
VII-A. 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. Rappelons les principales méthodes de cette API.
En mode connecté, l'application :
- ouvre une connexion avec la source de données
- travaille avec la source de données en lecture/écriture
- ferme la connexion
Trois interfaces ADO.NET sont principalement concernées par ces opérations :
- IDbConnection qui encapsule les propriétés et méthodes de la connexion.
- IDbCommand qui encapsule les propriétés et méthodes de la commande SQL exécutée.
- IDataReader qui encapsule les propriétés et méthodes du résultat d'un ordre SQL Select.
L'interface IDbConnection
Sert à gérer la connexion avec la base de données. Parmi les méthodes M et propriétés P de cette interface on trouve les suivantes :
Nom | Type | Rôle |
ConnectionString | P | chaîne de connexion à la base. Elle précise tous les paramètres nécessaires à l'établissement de la connexion avec une base précise. |
Open | M | ouvre la connexion avec la base définie par ConnectionString |
Close | M | ferme la connexion |
BeginTransaction | M | démarre une transaction. |
State | P | état de la connexion : ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken |
Si Connection est une classe implémentant l'interface IDbConnection, l'ouverture de la connexion peut se faire comme suit :
L'interface IDbCommand
Sert à exécuter un ordre SQL ou une procédure stockée. Parmi les méthodes M et propriétés P de cette interface on trouve les suivantes :
Nom | Type | Rôle |
CommandType | P | indique ce qu'il faut exécuter - prend ses valeurs dans une énumération : - CommandType.Text : exécute l'ordre SQL défini dans la propriété CommandText. C'est la valeur par défaut. - CommandType.StoredProcedure : exécute une procédure stockée dans la base |
CommandText | P | - le texte de l'ordre SQL à exécuter si CommandType= CommandType.Text - le nom de la procédure stockée à exécuter si CommandType= CommandType.StoredProcedure |
Connection | P | la connexion IDbConnection à utiliser pour exécuter l'ordre SQL |
Transaction | P | la transaction IDbTransaction dans laquelle exécuter l'ordre SQL |
Parameters | P | la liste des paramètres d'un ordre SQL paramétré. L'ordre update articles set prix=prix*1.1 where id=@id a le paramètre @id. |
ExecuteReader | M | pour exécuter un ordre SQL Select. On obtient un objet IDataReader représentant le résultat du Select. |
ExecuteNonQuery | M | pour exécuter un ordre SQL Update, Insert, Delete. On obtient le nombre de lignes affectées par l'opération (mises à jour, insérées, détruites). |
ExecuteScalar | M | pour exécuter un ordre SQL Select ne rendant qu'un unique résultat comme dans : select count(*) from articles. |
CreateParameter | M | pour créer les paramètres IDbParameter d'un ordre SQL paramétré. |
Prepare | M | permet d'optimiser l'exécution d'une requête paramétrée lorsqu'elle est exécutée de multiples fois avec des paramètres différents. |
Si Command est une classe implémentant l'interface IDbCommand, l'exécution d'un ordre SQL sans transaction aura la forme suivante :
L'interface IDataReader
Sert à encapsuler les résultats d'un ordre SQL Select. Un objet IDataReader représente une table avec des lignes et des colonnes, qu'on exploite séquentiellement : d'abord la 1re ligne, puis la seconde…. Parmi les méthodes M et propriétés P de cette interface on trouve les suivantes :
Nom | Type | Rôle |
FieldCount | P | le nombre de colonnes de la table IDataReader |
GetName | M | GetName(i) rend le nom de la colonne n° i de la table IDataReader. |
Item | P | Item[i] représente la colonne n° i de la ligne courante de la table IDataReader. |
Read | M | passe à la ligne suivante de la table IDataReader. Rend le booléen True si la lecture a pu se faire, False sinon. |
Close | M | ferme la table IDataReader. |
GetBoolean | M | GetBoolean(i) : rend la valeur booléenne de la colonne n° i de la ligne courante de la table IDataReader. Les autres méthodes analogues sont les suivantes : GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString. |
Getvalue | M | Getvalue(i) : rend la valeur de la colonne n° i de la ligne courante de la table IDataReader en tant que type object. |
IsDBNull | M | IsDBNull(i) rend True si la colonne n° i de la ligne courante de la table IDataReader n'a pas de valeur ce qui est symbolisé par la valeur SQL NULL. |
L'exploitation d'un objet IDataReader ressemble souvent à ce qui suit :
csharp | 1 | 1 | 1 | ||
// ouverture connexion IDbConnection connexion=… connexion.Open(); // préparation commande IDbCommand commande=new Command(); commande.Connection=connexion; // exécution ordre select commande.CommandText="select …"; IDataReader reader=commande.ExecuteReader(); // exploitation résultats while(reader.Read()){ // exploiter ligne courante … } // fermeture reader reader.Close(); // fermeture connexion connexion.Close(); |
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é.
VII-B. La base de données exemple▲
Pour montrer comment travailler avec NHibernate, nous utiliserons la base de données MySQL [dbpam_nhibernate] suivante :
- en [1, la base a trois tables :
- [employes] : une table qui enregistre les employées d'une crèche
- [cotisations] : une table qui enregistre des taux de cotisations sociales
- [indemnites] : une table qui enregistre des informations permettant de calculer la paie des employées
Table [employes]
- en [2], la table des employés et en [3], la signification de ses champs
Le contenu de la table pourrait être le suivant :
Table [cotisations]
- en [4], la table des cotisations et en [5], la signification de ses champs
Le contenu de la table pourrait être le suivant :
Table [indemnites]
- en [6], la table des indemnités et en [7], la signification de ses champs
Le contenu de la table pourrait être le suivant :
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.
VII-C. 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 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
VII-C-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 :
- lignes 4-7 : définissent des sections de configuration dans le fichier [App.config]. Considérons la ligne 6 :
xml | 1 | 1 | 1 | ||
<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 :
- 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.
- en [6], l'espace de noms de la classe [MySqlDataDriver] utilisé ligne 6 ci-dessous :
- 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 :
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].
VII-C-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 |
VII-C-2-a. 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 :
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 :
- 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 page 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].
VII-C-2-b. 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 :
Le fichier de mapping table [indemnites] <--> classe [Indemnites] pourrait être le suivant (Indemnites.hbm.xml) :
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].
VII-C-2-c. 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 :
Le fichier de mapping [Employe.hbm.xml] pourrait être le suivant :
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] :
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] :
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é.
VII-D. 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.
VII-D-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] :
Dans un code C#, la SessionFactory peut être obtenue de la façon suivante :
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.
VII-D-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 :
- 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).
VII-D-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); |
VII-D-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 :
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)
..
VII-E. 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
- [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.
VII-E-1. Obtenir le contenu de la base▲
Le programme [ShowDataBase.cs] permet d'afficher le contenu de la base :
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é.
VII-E-2. Insérer des données dans la base▲
Le programme [FillDataBase.cs] permet d'insérer des données dans la base :
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és dans la base de données
- ligne 112 : l'entité Cotisations est persisté à 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 :
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 :
VII-E-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 :
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 :
VII-E-4. Insertion d'entités invalides▲
La méthode suivante tente de sauvegarder une entité [Employe] non initialisée.
Explications
Rappelons le code de la classe [Employe] :
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 :
VII-E-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 :
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.
VII-E-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 :
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.
VII-F. Conclusion▲
Nous avons présenté les concepts importants de Nhibernate. Le code des exemples est disponible à l'Url
[https://tahe.developpez.com/dotnet/nhibernate] sous la forme d'un projet Visual Studio.
Le projet est accompagné de trois fichiers de configuration [1] :
- un pour le Sgbd MySQL [App.config.MySQL]
- un pour le Sgbd SQL Server [App.config.SQLServer]
- un pour le Sgbd SQL Server Compact [App.config.SQLServerCe]
Pour les utiliser, il suffit de remplacer [App.config] par le fichier approprié.
En [2], le dossier complet qui accompagne ce document :
- le dossier [databases] [3] contient
- un script SQL pour générer la base MySQL
- une base de données SQL Server 2005
- une base de données SQL Server Compact 3.5
- le dossier [lib] [5] contient les DLL nécessaires au projet Visual Studio.
- le dossier [pam-nhibernate-demos] est celui de la solution Visual Studio 2010.