Skip to content

6. Introduzione a ORM NHibernate

Questo capitolo è una breve introduzione a NHibernate, l’equivalente per .NET del framework Java Hibernate. Per un’introduzione completa si può consultare:


Titolo: NHibernate in Action, Autore: Pierre-Henri Kuaté, Editore: Manning, ISBN-13: 978-1932394924


Un ORM (Object Relational Mapper) è un insieme di librerie che consente a un programma che utilizza un database di interagirvi senza emettere comandi SQL espliciti e senza conoscere le specificità del SGBD utilizzato.


Prerequisiti


In una struttura [débutant-intermédiaire-avancé], questo documento si trova nella sezione [intermédiaire]. Per comprenderlo sono necessari diversi prerequisiti che è possibile trovare in alcuni dei documenti che ho scritto:

  1. Linguaggio C# 2008: [Apprentissage du langage C# Version 3.0 avec le Framework .NET 3.5 ]
  2. [Spring IoC], disponibile su URL e [Spring IoC pour .NET ]. Presenta le basi dell’inversione di controllo (Inversion of Control) o dell’iniezione di dipendenze (Dependency Injection) del framework Spring.Net [Spring.NET | Homepage ].

All'inizio dei paragrafi del presente documento vengono talvolta forniti suggerimenti di lettura che rimandano ai documenti precedenti.


Strumenti


Gli strumenti utilizzati in questo caso di studio sono disponibili gratuitamente sul web. Si tratta dei seguenti (dicembre 2011):

  • Nhibernate 3.2 disponibile all'URL [http://nhforge.org/Default.aspx]
  • Spring.net 1.3.2 disponibile all'URL [http://www.springframework.net]. Il framework Spring.net è molto completo. In questa sede utilizzeremo solo la libreria che esso fornisce per facilitare l'uso del framework Nhibernate.
  • Log4net 1.2.10 disponibile all'URL [http://logging.apache.org/log4net]. Questo framework di log viene utilizzato da Nhibernate.
  • Nunit 2.5 è disponibile all'URL [http://www.nunit.org/]. Questo framework per i test unitari è l'equivalente per .NET del framework JUnit per la piattaforma Java.
  • Il driver ADO.NET 6.4.4 di SGBD MySQL 5 è disponibile all'URL [http://dev.mysql.com/downloads/connector/net]

Tutti i file DLL necessari per i progetti Visual Studio 2010 sono stati raccolti in una cartella [libnet4]:

 

6.1. Il ruolo di NHIBERNATE in un'architettura a livelli .NET

Un’applicazione .NET che utilizza un database può essere strutturata a livelli nel modo seguente:

Il livello [dao] comunica con il SGBD tramite il API ADO.NET (vedere paragrafo 3.3).Nell’architettura precedente, il connettore [ADO.NET] è collegato al SGBD. Pertanto, la classe che implementa l’interfaccia [IDbConnection] è:

  • la classe [MySQLConnection] per SGBD e MySQL
  • la classe [SQLConnection] per SGBD e SQLServer

Il livello [dao] dipende quindi dal SGBD utilizzato. Alcuni framework (Linq, Ibatis.net, NHibernate) eliminano questa limitazione aggiungendo un livello supplementare tra il livello [dao] e il connettore [ADO.NET] del SGBD utilizzato. In questo caso utilizzeremo il framework [NHibernate].

Nell’esempio sopra riportato, il livello [dao] non si rivolge più al connettore [ADO.NET], ma al framework NHibernate, che gli fornirà un’interfaccia indipendente dal connettore [ADO.NET] utilizzato. Questa architettura consente di sostituire il SGBD senza modificare il livello [dao]. In tal caso, dovrà essere sostituito solo il connettore [ADO.NET].

6.2. Il database di esempio

Per illustrare come lavorare con NHibernate, utilizzeremo il seguente database MySQL [dbpam_nhibernate] descritto al paragrafo 3.1. L’esportazione della struttura del database in un file SQL fornisce il seguente risultato:

#
# Struttura della tabella `cotisations`: 
#

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;

#
# Struttura della tabella `indemnites`: 
#

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;

#
# Struttura della tabella `dipendenti`: 
#

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;

Si noti, alle righe 6, 20 e 36, che le chiavi primarie ID hanno l’attributo autoincrement. Ciò significa che MySQL genererà automaticamente i valori delle chiavi primarie ogni volta che viene aggiunto un record. Lo sviluppatore non deve preoccuparsene.

6.3. Il progetto C# di dimostrazione

Per illustrare la configurazione e l’utilizzo di NHibernate, utilizzeremo la seguente architettura:

Un programma da console [1] gestirà i dati del database precedente [2] tramite il framework [NHibernate] [3]. Questo ci porterà a presentare:

  • i file di configurazione di NHibernate
  • API di NHibernate

Il progetto C# sarà il seguente:

Gli elementi necessari per il progetto sono i seguenti:

  • in [1], i file DLL necessari al progetto:
    • [NHibernate]: il DLL del framework NHibernate
    • [MySql.Data]: il DLL del connettore ADO.NET del SGBD MySQL
    • [log4net]: il DLL del framework Log4net che consente di generare log
  • in [2], le classi che rappresentano le tabelle del database
  • in [3], il file [App.config] che configura l'intera applicazione, compreso il framework [NHibernate]
  • in [4], le applicazioni console di test

6.3.1. Configurazione della connessione al database

Torniamo all’architettura di test:

Come illustrato sopra, [NHibernate] deve poter accedere al database. A tal fine, necessita di alcune informazioni:

  • il SGBD che gestisce il database (MySQL, SQLServer, Postgres, Oracle, ...). La maggior parte dei SGBD ha aggiunto al linguaggio SQL delle estensioni proprie. Conoscendo il SGBD, il NHibernate può adattare i comandi SQL che emette a tale SGBD. NHibernate utilizza il concetto di dialetto SQL.
  • i parametri di connessione al database (nome del database, nome dell’utente titolare della connessione, relativa password)

Queste informazioni possono essere inserite nel file di configurazione [App.config]. Ecco quello che verrà utilizzato con un database MySQL 5:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <!-- Sezioni di configurazione -->
    <configSections>
        <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
        <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
    </configSections>


    <!-- Configurazione 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>

    <!-- Questa sezione contiene le impostazioni di configurazione di log4net -->
    <!-- NOTE IMPORTANTE: i log non sono attivi per impostazione predefinita. È necessario attivarli a livello di programma con l'istruzione log4net.Config.XmlConfigurator.Configure();
    ! -->
    <log4net>
        <!-- Definire un appender di output (dove devono essere inviati i log) -->
        <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>

        <!-- Configurare la categoria principale, impostare il livello di priorità predefinito e aggiungere gli appender (dove verranno inviati i log) -->
        <root>
            <priority value="INFO" />
            <!--
            <appender-ref ref="LogFileAppender" />
            <appender-ref ref="LogDebugAppender"/>
            -->
            <appender-ref ref="ConsoleAppender"/>
        </root>

        <!-- Specificare il livello per alcuni spazi dei nomi specifici -->
        <!-- Il livello può essere: ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF -->
        <logger name="NHibernate">
            <level value="INFO" />
        </logger>
    </log4net>
</configuration>
  • righe 4-7: definiscono sezioni di configurazione nel file [App.config]. Consideriamo la riga 6:

<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />

Questa riga definisce la sezione di configurazione di NHibernate nel file [App.config]. Presenta due attributi: name e type.

  • L'attributo [name] assegna un nome alla sezione di configurazione. Tale sezione deve essere delimitata dai tag <name>...</name>, in questo caso <hibernate-configuration>...</hibernate-configuration>, nelle righe 11-24.
  • L'attributo [type=classe,DLL] indica il nome della classe incaricata di gestire la sezione definita dall'attributo [name], nonché la DLL contenente tale classe. In questo caso, la classe si chiama [NHibernate.Cfg.ConfigurationSectionHandler] e si trova nella DLL [NHibernate.dll]. Ricordiamo che questa DLL fa parte dei riferimenti del progetto in esame.

Consideriamo ora la sezione di configurazione di NHibernate:


    <!-- configurazione 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>
  • riga 2: la configurazione di NHibernate si trova all’interno di un tag <hibernate-configuration>. L’attributo xmlns (Xml NameSpace) specifica la versione utilizzata per configurare NHibernate. Infatti, nel corso del tempo, il modo di configurare NHibernate si è evoluto. In questo caso, viene utilizzata la versione 2.2.
  • riga 3: la configurazione di NHibernate è qui interamente contenuta nel tag <session-factory> (righe 3 e 14). Una sessione NHibernate è lo strumento utilizzato per lavorare con un database secondo lo schema:
    • apertura della sessione
    • lavoro con il database tramite i metodi di API NHibernate
    • chiusura della sessione

La sessione viene creata da un factory, un termine generico che indica una classe in grado di creare oggetti. Le righe 3-14 configurano questo factory.

  • righe 4, 6, 8, 9: configurano la connessione al database di destinazione. Le informazioni principali sono il nome del SGBD utilizzato, il nome del database, l’identità dell’utente e la sua password.
  • riga 4: definisce il provider della connessione, ovvero quello a cui si richiede una connessione al database. Il valore della proprietà [connection.provider] è il nome di una classe NHibernate. Questa proprietà non dipende dal SGBD utilizzato.
  • riga 6: il driver ADO.NET da utilizzare. Si tratta del nome di una classe NHibernate specializzata per un determinato SGBD, in questo caso MySQL. La riga 6 è stata commentata, poiché non è indispensabile.
  • Riga 8: la proprietà [dialect] imposta il dialetto SQL da utilizzare con SGBD. In questo caso si tratta del dialetto di SGBD MySQL.

Se si cambia il SGBD, come si individua il dialetto NHibernate corrispondente? Torniamo al progetto C# precedente e facciamo doppio clic su DLL [NHibernate] nella scheda [References]:

  • in [1], la scheda [Explorateur d'objets] mostra una serie di DLL, tra cui quelle a cui fa riferimento il progetto.
  • in [2], il DLL [NHibernate]
  • in [3], la DLL e la [NHibernate] sviluppata. Qui si trovano i diversi spazi dei nomi (namespace) che vi sono definiti.
  • in [4], lo spazio dei nomi [NHibernate.Dialect] in cui si trovano le classi che definiscono i diversi dialetti SQL utilizzabili.
  • in [5], la classe del dialetto di SGBD MySQL 5.
  • in [6], lo spazio dei nomi della classe [MySqlDataDriver] utilizzata alla riga 6 qui sotto:

    <!-- configurazione 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>
  • righe 9-11: la stringa di connessione al database. Questa stringa ha la forma "param1=val1;param2=val2; ...". L'insieme dei parametri così definiti consente al driver di SGBD di stabilire una connessione. La struttura di questa stringa di connessione dipende dal SGBD utilizzato. Le stringhe di connessione ai principali SGBD sono disponibili sul sito [http://www.connectionstrings.com/]. In questo caso, la stringa "Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;" è una stringa di connessione per il SGBD MySQL. Essa indica che:
    • Server=localhost;: il SGBD si trova sulla stessa macchina del client che sta tentando di aprire la connessione
    • Database=dbpam_nhibernate; : il database MySQL di destinazione
    • Uid=root; : l'utente che apre la connessione è l'utente root
    • Pwd=;: questo utente non ha una password (caso particolare di questo esempio)
  • riga 12: la proprietà [show_sql] indica se NHibernate deve riportare nei propri log i comandi SQL che invia al database. In fase di sviluppo, è utile impostare questa proprietà su [true] per sapere esattamente cosa fa NHibernate.
  • riga 13: per comprendere il tag <mapping>, torniamo all’architettura dell’applicazione:

Se il programma da console fosse un client diretto del connettore ADO.NET e volesse l’elenco dei dipendenti, farebbe eseguire al connettore un comando SQL Select, e riceverebbe in risposta un oggetto di tipo IDataReader che dovrebbe elaborare per ottenere l'elenco dei dipendenti inizialmente richiesto.

Nell’esempio sopra riportato, il programma da console è il client di NHibernate, mentre NHibernate è il client del connettore ADO.NET. Vedremo in seguito che il API di NHibernate consentirà al programma da console di richiedere l’elenco dei dipendenti. NHibernate tradurrà questa richiesta in un comando SQL Select che farà eseguire al connettore ADO.NET. Quest’ultimo restituirà un oggetto di tipo IDataReader. A partire da questo oggetto, NHibernate deve essere in grado di costruire l’elenco dei dipendenti che gli è stato richiesto. Ciò è reso possibile dalla configurazione. A ogni tabella del database è associata una classe C#. Pertanto, a partire dalle righe della tabella [employes] restituite da IDataReader, NHibernate sarà in grado di costruire un elenco di oggetti che rappresentano i dipendenti e di restituirlo al programma console. Queste relazioni tra tabelle e classi vengono create nei file di configurazione. NHibernate utilizza il termine «mapping» per definire tali relazioni.

Torniamo alla riga 13 qui sotto:


    <!-- configurazione 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 riga 13 indica che i file di configurazione tabelle <--> classi si troveranno nell’assembly [pam-nhibernate-demos]. Un assembly è l’eseguibile o il DLL prodotto dalla compilazione di un progetto. In questo caso, i file di mappatura saranno inseriti nell’assembly del progetto di esempio. Per conoscere il nome di questo assembly, occorre consultare le proprietà del progetto:

  • in [1], le proprietà del progetto
  • nella scheda [Application] [2], il nome dell'assembly [3] che verrà generato.
  • Poiché il tipo di output è [Application console] [4], il file generato durante la compilazione del progetto si chiamerà [pam-nhibernate-demos.exe]. Se il tipo di output fosse [Bibliothèque de classes] [5], il file generato durante la compilazione del progetto si chiamerebbe [pam-nhibernate-demos.dll]
  • l'assembly viene generato nella cartella [bin/Release] del progetto [6].

Dalla spiegazione precedente si evince che i file delle tabelle di mappatura <--> classi dovranno trovarsi nel file [pam-nhibernate-demos.exe] [6].

6.3.2. Configurazione del file di mappatura " " <--> classi

Torniamo all’architettura del progetto in esame:

  • nel file [1] il programma da console utilizza i metodi del file API del framework NHibernate. Questi due blocchi si scambiano oggetti.
  • In [2], NHibernate utilizza API di un connettore .NET. Invia comandi SQL al SGBD di destinazione.

Il programma da console gestirà oggetti che rispecchiano le tabelle del database. In questo progetto, tali oggetti e i collegamenti che li legano alle tabelle del database sono stati inseriti nella cartella [Entites] riportata di seguito:

 
  • Ogni tabella del database è rappresentata da una classe e da un file di mappatura tra le due
Tabella
Classe
Mappatura
contributi
Cotisations.cs
Cotisations.hbm.xml
dipendenti
Employe.cs
Employe.hbm.xml
indennità
Indemnites.cs
Indemnites.hbm.xml

6.3.2.1. Mappatura della tabella [cotisations]

Consideriamo la tabella [cotisations]:

ID
chiave primaria di tipo autoincrement
VERSION
numero di versione del record
SECU
aliquota (percentuale) dei contributi previdenziali
RETRAITE
aliquota contributiva per la pensione
CSGD
aliquota contributiva per il contributo sociale generalizzato deducibile
CSGRDS
aliquota contributiva per il contributo sociale generalizzato e il contributo al rimborso del debito sociale

Una riga di questa tabella può essere incapsulata in un oggetto di tipo [Cotisations.cs] come segue:


namespace PamNHibernateDemos {
    public class Cotisations {
        // proprietà automatiche
        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; }

        // costruttori
        public Cotisations() {
        }
        // ToString
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}]", CsgRds, Csgd, Secu, Retraite);
        }
    }

}

È stata creata una proprietà automatica per ciascuna delle colonne della tabella [cotisations]. Ciascuna di queste proprietà deve essere dichiarata virtuale (virtual) poiché NHibernate è destinata a derivare dalla classe e a ridefinirne (override) le proprietà. Queste devono quindi essere virtuali.

Si noti, alla riga 1, che la classe appartiene allo spazio dei nomi [PamNHibernateDemos].

Il file di mappatura [Cotisations.hbm.xml] tra la tabella [cotisations] e la classe [Cotisations] è il seguente:


<?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>
  • il file di mappatura è un file XML definito all’interno del tag <hibernate-mapping> (righe 2 e 14)
  • riga 4: il tag <class> collega una tabella del database a una classe. In questo caso, la tabella [COTISATIONS] (attributo table) e la classe [Cotisations] (attributo name). In .NET, una classe deve essere definita tramite il suo nome completo (spazio dei nomi incluso) e tramite l’assembly che la contiene. Queste due informazioni sono fornite dalla riga 3. La prima (spazio dei nomi) può essere reperita nella definizione della classe. La seconda (assembly) è il nome dell’assembly del progetto. Abbiamo già indicato come trovare questo nome.
  • righe 5-7: il tag <id> serve a definire la mappatura della chiave primaria della tabella [cotisations].
    • riga 5: l'attributo name indica il campo della classe [Cotisations] che conterrà la chiave primaria della tabella [cotisations]. L'attributo column indica la colonna della tabella [cotisations] che funge da chiave primaria. L'attributo unsaved-value serve a definire una chiave primaria non ancora generata. Questo valore consente a NHibernate di sapere come salvare un oggetto [Cotisations] nella tabella [cotisations]. Se tale oggetto ha un campo Id=0, eseguirà un'operazione SQL INSERT, altrimenti eseguirà un'operazione SQL UPDATE. Il valore di unsaved-value dipende dal tipo del campo Id della classe [Cotisations]. In questo caso, è di tipo int e il valore predefinito di un tipo int è 0. Un oggetto [Cotisations] non ancora salvato (quindi privo di chiave primaria) avrà quindi il campo Id pari a 0. Se il campo Id fosse stato di tipo Object o derivato, si sarebbe scritto unsaved-value=null.
    • riga 6: quando NHibernate deve salvare un oggetto [Cotisations] con un campo Id=0, deve eseguire sul database un’operazione INSERT durante la quale deve ottenere un valore per la chiave primaria del record. La maggior parte dei SGBD dispone di un metodo proprietario per generare automaticamente tale valore. Il tag <generator> serve a definire il meccanismo da utilizzare per la generazione della chiave primaria. Il tag <generator class="native"> indica che occorre utilizzare il meccanismo predefinito del SGBD utilizzato. Abbiamo visto nel paragrafo 6.2 che le chiavi primarie delle nostre tre tabelle MySQL avevano l’attributo autoincrement. Durante le operazioni INSERT, NHibernate non fornirà alcun valore alla colonna ID del record aggiunto, lasciando che sia MySQL a generare tale valore.
  • riga 8: il tag <version> serve a definire la colonna della tabella (nonché il campo della classe corrispondente) che consente di “versionarizzare” i record. Inizialmente, la versione è pari a 1. Viene incrementata ad ogni operazione UPDATE. D’altra parte, ogni operazione UPDATE o DELETE viene eseguita con un filtro WHERE ID= id AND VERSION=v1. Un utente può quindi modificare o eliminare un oggetto solo se ne possiede la versione corretta. In caso contrario, viene generata un'eccezione da NHibernate.
  • riga 9: il tag <property> serve a definire una mappatura di colonna normale (né chiave primaria, né colonna di versione). Pertanto, la riga 9 indica che la colonna CSGRDS della tabella [COTISATIONS] è associata alla proprietà CsgRds della classe [Cotisations].

6.3.2.2. Mappatura della tabella [indemnites]

Consideriamo la tabella [indemnites]:

ID
chiave primaria di tipo autoincrement
VERSION
numero di versione del record
BASE_HEURE
costo in euro di un'ora di guardia
ENTRETIEN_JOUR
indennità in euro per giorno di guardia
REPAS_JOUR
indennità pasto in euro per giorno di servizio
INDEMNITES_CP
indennità per ferie retribuite. Si tratta di una percentuale da applicare alla retribuzione base.

Una riga di questa tabella può essere racchiusa in un oggetto di tipo [Indemnites] come segue:


namespace PamNHibernateDemos {
    public class Indemnites {

        // proprietà automatiche
        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; }

        // costruttori
        public Indemnites() {
        }

        // identità
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}|{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
        }

    }
}

Il file di mappatura della tabella [indemnites] <--> classe [Indemnites] potrebbe essere il seguente (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>

Non vi è nulla di nuovo rispetto al file di mappatura spiegato in precedenza. L’unica differenza si trova alla riga 9. L'attributo unique="true" indica che nella tabella [indemnites] è presente un vincolo di unicità sulla colonna [INDICE]: non possono esserci due righe con lo stesso valore per la colonna [INDICE].

6.3.2.3. Mappatura della tabella [employes]

Consideriamo la tabella [employes]:

ID
chiave primaria di tipo autoincrement
VERSION
numero di versione del record
PRENOM
nome del dipendente
NOM
il suo cognome
ADRESSE
il suo indirizzo
CP
il suo codice postale
VILLE
la sua città
INDEMNITE_ID
chiave esterna su INDEMNITES(ID)

La novità rispetto alle tabelle precedenti è la presenza di una chiave esterna: la colonna [INDEMNITE_ID] è una chiave esterna sulla colonna [ID] della tabella [INDEMNITES]. Questo campo fa riferimento alla riga della tabella [INDEMNITES] da utilizzare per il calcolo delle indennità del dipendente.

La classe [Employe] 'immagine della tabella [employes] potrebbe essere la seguente:


namespace PamNHibernateDemos {
    public class Employe {
        // proprietà automatiche
        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; }

        // costruttori
        public Employe() {
        }

        // ToString
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
        }
    }
}

Il file di mappatura [Employe.hbm.xml] potrebbe essere il seguente:


<?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 novità si trova alla riga 15 con la comparsa di un nuovo tag: <many-to-one>. Questo tag serve a mappare una colonna chiave esterna [INDEMNITE_ID] della tabella [EMPLOYES] alla proprietà [Indemnites] della classe [Employe]:


namespace PamNHibernateDemos {
    public class Employe {
        // proprietà automatiche
..
        public virtual Indemnites Indemnites { get; set; }

...
    }
}

La tabella [EMPLOYES] ha una chiave esterna [INDEMNITE_ID] che fa riferimento alla colonna [ID] della tabella [INDEMNITES]. Diverse (many) righe della tabella [EMPLOYES] possono fare riferimento a una stessa riga (one) della tabella [INDEMNITES]. Da qui il nome del tag <many-to-one>. Questo tag presenta qui i seguenti attributi:

  • column: indica il nome della colonna della tabella [EMPLOYES] che funge da chiave esterna nella tabella [INDEMNITES]
  • name: indica la proprietà della classe [Employe] associata a questa colonna. Il tipo di questa proprietà è necessariamente la classe associata alla tabella di destinazione della chiave esterna, in questo caso la tabella [INDEMNITES]. Sappiamo che questa classe è la classe [Indemnites] già descritta. Questo è quanto riflette la riga 5 sopra riportata. Ciò significa che quando NHibernate recupererà dal database un oggetto [Employe], recupererà anche l’oggetto [Indemnites] ad esso associato.
  • cascata: questo attributo può assumere diversi valori:
    • save-update: un'operazione di inserimento (save) o di aggiornamento (update) sull'oggetto [Employe] deve essere propagata all'oggetto [Indemnites] in esso contenuto.
    • delete: l'eliminazione di un oggetto [Employe] deve essere propagata all'oggetto [Indemnites] che contiene.
    • all: propaga le operazioni di inserimento (save), aggiornamento (update) ed eliminazione (delete).
    • none: non propaga nulla

Per concludere, ricordiamo la configurazione di NHibernate nel file [App.config]:


    <!-- configurazione 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 riga 13 indica che i file di mappatura *.hbm.xml si troveranno nell’assembly [pam-nhibernate-demos]. Questa impostazione non è predefinita. È necessario configurarla nel progetto C#:

  • in [1], si selezionano le proprietà di un file di mappatura
  • in [2], l'azione di generazione deve essere [Ressource incorporée] [3]. Ciò significa che, al momento della generazione del progetto, il file di mappatura deve essere incorporato nell'assembly generato.

6.4. da API a NHibernate

Torniamo all’architettura del nostro progetto di esempio:

Nei paragrafi precedenti abbiamo configurato NHibernate in due modi:

  • in [App.config] abbiamo configurato la connessione al database
  • abbiamo definito, per ogni tabella del database, la classe che la rappresenta e il file di mappatura che consente di passare dalla classe alla tabella e viceversa.

Non ci resta che scoprire i metodi offerti da NHibernate per gestire i dati del database: inserimento, aggiornamento, cancellazione, elenco.

6.4.1. L'oggetto SessionFactory

Ogni operazione NHibernate viene eseguita all’interno di una sessione. Una sequenza tipica di operazioni NHibernate è la seguente:

  • aprire una sessione NHibernate
  • avviare una transazione nella sessione
  • eseguire operazioni di persistenza con la sessione (Load, Get, Find, CreateQuery, Save, SaveOrUpdate, Delete)
  • convalidare (commit) o annullare (rollback) la transazione
  • chiudere la sessione NHibernate

Una sessione viene ottenuta da una factory di tipo [SessionFactory]. Questa factory è quella configurata dal tag <session-factory> nel file di configurazione [App.config]:


    <!-- configurazione 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>

In un codice C#, la SessionFactory può essere ottenuta nel modo seguente:


ISessionFactory sessionFactory = new Configuration().Configure().BuildSessionFactory();

La classe Configuration è una classe del framework NHibernate. L'istruzione precedente utilizza la sezione di configurazione di NHibernate in [App.config]. L'oggetto [ISessionFactory] ottenuto contiene quindi le:

  • le informazioni necessarie per creare una connessione al database di destinazione
  • file di mappatura tra le tabelle del database e le classi persistenti gestite da NHibernate.

6.4.2. La sessione NHibernate

Una volta creato SessionFactory (operazione che va eseguita una sola volta), è possibile ottenere le sessioni che consentono di eseguire operazioni di persistenza NHibernate. Un codice tipico è il seguente:


try{
      // accesso 
      using (ISession session = sessionFactory.OpenSession())
      {
        // inizio transazione
        using (ITransaction transaction = session.BeginTransaction())
        {
........................ opérations de persistance
          // convalida della transazione
          transaction.Commit();
        }
      }
}catch (Exception ex){
....
}
  • riga 3: viene creata una sessione a partire da SessionFactory all’interno di una clausola using. All'uscita dalla clausola using, la sessione verrà chiusa automaticamente. Senza la clausola using, sarebbe necessario chiudere la sessione in modo esplicito (session.Close()).
  • riga 6: le operazioni di persistenza verranno eseguite all'interno di una transazione. O vanno tutte a buon fine, oppure nessuna va a buon fine. All'interno della clausola using,, la transazione viene convalidata da un Commit (riga 10). Se all’interno della transazione un’operazione di persistenza genera un’eccezione, la transazione verrà automaticamente invalidata da un Rollback all’uscita dal using.
  • Il blocco try/catch delle righe 1 e 13 consente di intercettare un'eventuale eccezione generata dal codice all'interno del blocco try (sessione, transazione, persistenza).

6.4.3. L'interfaccia ISession

Presentiamo ora alcuni dei metodi dell’interfaccia ISession implementata da una sessione NHibernate:

ITransaction BeginTransaction()
avvia una transazione nella sessione
ITransaction tx=session.BeginTransaction();
void Clear()
svuota la sessione. Gli oggetti in essa contenuti diventano distaccati.
session.Clear();
void Close()
chiude la sessione. Gli oggetti in essa contenuti vengono sincronizzati con il database. Questa operazione di sincronizzazione viene eseguita anche al termine di una transazione. Quest'ultimo caso è il più comune.
session.Close();
IQuery CreateQuery(string queryString)
crea una query HQL (Hibernate Query Language) per un'esecuzione successiva.
IQuery query=session.createQuery("select e from Employe e);
void Delete(object obj)
elimina un oggetto. Quest’ultimo può appartenere alla sessione (associato) oppure no (non associato). Durante la sincronizzazione della sessione con il database, su questo oggetto verrà eseguita un’operazione SQL DELETE.
// si carica un dipendente da BD
Dipendente e = session.Get<Dipendente>(143);
// lo si elimina
session.Delete(e);
void Flush()
forza la sincronizzazione della sessione con il database. Il contenuto della sessione non cambia.
session.Flush();
T Get<T>(object id)
recupera dal database l'oggetto T con chiave primaria id. Se tale oggetto non esiste, imposta il puntatore a null.
// si carica un dipendente da BD
Dipendente e = session.Get<Dipendente>(143);
object Save(object obj)
inserisce l'oggetto obj nella sessione. Questo oggetto non ha una chiave primaria prima del Save. Dopo il Save, ne ha una. Durante la sincronizzazione della sessione, verrà eseguita un'operazione SQL INSERT sul database.
// si crea un dipendente
Dipendente e = new Dipendente(){...};
// lo si salva
e = session.Save(e);
SaveOrUpdate(object obj)
esegue l'operazione Save se l'oggetto non ha una chiave primaria oppure l'operazione Update se ne ha già una.
void Update(object obj)
aggiorna l’oggetto obj nel database. Viene quindi eseguita un’operazione SQL UPDATE sul database.
// si carica un dipendente da BD
Dipendente e = session.Get<Dipendente>(143);
// si modifica il suo nome
e.Nom = ...;
// lo si aggiorna nel database
session.Update(e);

6.4.4. L'interfaccia IQuery

L'interfaccia IQuery consente di eseguire query sul database per estrarne i dati. Abbiamo visto come crearne un'istanza:

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

Il parametro del metodo createQuery è una query HQL (Hibernate Query Language), un linguaggio analogo al linguaggio SQL ma che interroga le classi anziché le tabelle. La query sopra riportata richiede l'elenco di tutti i dipendenti. Ecco alcuni esempi di query 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

Presentiamo ora alcuni dei metodi dell’interfaccia IQuery:

IList<T> List<T>()
restituisce il risultato della query sotto forma di un elenco di oggetti T
IList<Employe> employes=session.createQuery("select e from Employe e order by e.Nom asc").List<Employe>();
IList List()
restituisce il risultato della query sotto forma di un elenco in cui ogni elemento dell'elenco rappresenta una riga di risultato della query Select sotto forma di un array di oggetti.
IList righe = session.createQuery("select e.Nom, e.Prenom, e.SS from Employe e").List();
righe[i][j] rappresenta la colonna j della riga i in un tipo object. Pertanto, righe[10][1] è un tipo object che rappresenta il nome di una persona. In genere sono necessarie conversioni di tipo per recuperare i dati nel loro tipo esatto.
T UniqueResult<T>()
restituisce il primo oggetto del risultato della query
Employe e=session.createQuery("select e from Employe e where e.Nom='MARTIN'").UniqueResult<Employe>();

È possibile configurare una query HQL:

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

Nella query HQL alla riga 3, :num è un parametro che deve ricevere un valore prima che la query venga eseguita. Nell’esempio sopra riportato, a tale scopo viene utilizzato il metodo SetString. L’interfaccia IQuery dispone di vari metodi Set per assegnare un valore a un parametro:

  • - SetBoolean(string name, bool value)
  • - SetSingle(string name, single value)
  • - SetDouble(string name, double value)
  • - SetInt32(string name, int32 value)
  • ..

6.5. Alcuni esempi di codice

Gli esempi che seguono si basano sull'architettura analizzata in precedenza e riportata di seguito. Il database è il database MySQL [dbpam_nhibernate], anch'esso presentato. Gli esempi sono programmi da console [1] che utilizzano il framework NHibernate [3] per gestire il database [2].

Il progetto C# in cui sono inseriti gli esempi che seguiranno è quello già presentato:

  • in [1], i file DLL necessari al progetto:
    • [NHibernate]: il file DLL del framework NHibernate
    • [MySql.Data]: il DLL del connettore ADO.NET del SGBD MySQL 5
    • [log4net]: il DLL di uno strumento che consente di generare log
  • in [2], le classi delle immagini delle tabelle del database
  • in [3], il file [App.config] che configura l'intera applicazione, compreso il framework [NHibernate]
  • in [4], e le applicazioni console di test. Sono proprio queste ultime che presenteremo in parte.

6.5.1. Ottenere il contenuto del database

Il programma [ShowDataBase.cs] consente di visualizzare il contenuto del database:


using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;


namespace PamNHibernateDemos
{
  public class ShowDataBase
  {

    private static ISessionFactory sessionFactory = null;

    // programma principale
    static void Main(string[] args)
    {
      // inizializzazione factory NHibernate
      sessionFactory = new Configuration().Configure().BuildSessionFactory();
      try
      {
        // visualizzazione del contenuto del database
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase1();
      }
      catch (Exception ex)
      {
        // visualizzazione dell'eccezione 
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
      }
      finally
      {
        if (sessionFactory != null)
        {
          sessionFactory.Close();
        }
      }
      // attesa input da tastiera
      Console.ReadLine();
    }

    // test1
    static void ShowDataBase1()
    {
      // accesso 
      using (ISession session = sessionFactory.OpenSession())
      {
        // inizio transazione
        using (ITransaction transaction = session.BeginTransaction())
        {
          // si recupera l'elenco dei dipendenti
          IList<Employe> employes = session.CreateQuery(@"select e from Employe e order by e.Nom asc").List<Employe>();
          // visualizzazione
          Console.WriteLine("--------------- liste des employés");
          foreach (Employe e in employes)
          {
            Console.WriteLine(e);
          }
          // si recupera l'elenco delle indennità
          IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i order by i.Indice asc").List<Indemnites>();
          // visualizzazione
          Console.WriteLine("--------------- liste des indemnités");
          foreach (Indemnites i in indemnites)
          {
            Console.WriteLine(i);
          }
          // si recupera l'elenco dei contributi
          Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
          Console.WriteLine("--------------- tableau des taux de cotisations");
          Console.WriteLine(cotisations);
          // conferma transazione
          transaction.Commit();
        }
      }
    }
  }
}

Spiegazioni:

  • riga 19: viene creato l'oggetto SessionFactory. È proprio questo che ci consentirà di ottenere gli oggetti Session.
  • riga 24: viene visualizzato il contenuto del database
  • righe 31-37: l'oggetto SessionFactory viene chiuso nella clausola finally dell'oggetto try.
  • riga 43: il metodo che visualizza il contenuto del database
  • riga 46: si ottiene una Session dalla SessionFactory.
  • riga 49: si avvia una transazione
  • riga 52: query HQL per recuperare l'elenco dei dipendenti. Grazie alla chiave esterna che collega l'entità Employe all'entità Indemnite, per ogni dipendente si otterrà la relativa indennità.
  • riga 60: query HQL per ottenere l'elenco delle indennità.
  • riga 68: query HQL per ottenere l'unica riga della tabella dei contributi.
  • riga 72: fine della transazione
  • riga 73: fine dell'istruzione «using Itransaction» della riga 49 – la transazione viene chiusa automaticamente
  • riga 74: fine dell'using Isession della riga 46 – la sessione viene chiusa automaticamente.

Risultato visualizzato sullo schermo:

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]

Si noti che alle righe 3 e 4, richiedendo un dipendente, è stata ottenuta anche la sua indennità.

6.5.2. Inserimento dei dati nel database

Il programma [FillDataBase.cs] consente di inserire dati nel database:


using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;


namespace PamNHibernateDemos
{
  public class FillDataBase
  {

    private static ISessionFactory sessionFactory = null;

    // programma principale
    static void Main(string[] args)
    {
      // inizializzazione della factory NHibernate
      sessionFactory = new Configuration().Configure().BuildSessionFactory();
      try
      {
        // cancellazione del contenuto del database
        Console.WriteLine("Effacement base -------------------------------------");
        ClearDataBase1();
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase();
        Console.WriteLine("Remplissage base -------------------------------------");
        FillDataBase1();
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase();
      }
      catch (Exception ex)
      {
        // visualizzazione dell'eccezione 
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
      }
      finally
      {
        if (sessionFactory != null)
        {
          sessionFactory.Close();
        }
      }
      // attesa da tastiera
      Console.ReadLine();
    }

    // test1
    static void ShowDataBase()
    {
             // vedi esempio precedente
    }

    // ClearDataBase1
    static void ClearDataBase1()
    {
      // accesso 
      using (ISession session = sessionFactory.OpenSession())
      {
        // inizio transazione
        using (ITransaction transaction = session.BeginTransaction())
        {
          // si recupera l'elenco dei dipendenti
          IList<Employe> employes = session.CreateQuery(@"select e from Employe e").List<Employe>();
          // si eliminano tutti i dipendenti
          Console.WriteLine("--------------- suppression des employés associés");
          foreach (Employe e in employes)
          {
            session.Delete(e);
          }
          // si recupera l'elenco delle indennità
          IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i").List<Indemnites>();
          // si eliminano le indennità
          Console.WriteLine("--------------- suppression des indemnités");
          foreach (Indemnites i in indemnites)
          {
            session.Delete(i);
          }
          // si recupera l'elenco dei contributi
          Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
          Console.WriteLine("--------------- suppression des taux de cotisations");
          if (cotisations != null)
          {
            session.Delete(cotisations);
          }
          // conferma transazione
          transaction.Commit();
        }
      }
    }

    // FillDataBase
    static void FillDataBase1()
    {
      // accesso 
      using (ISession session = sessionFactory.OpenSession())
      {
        // inizio transazione
        using (ITransaction transaction = session.BeginTransaction())
        {
          // si creano due indennità
          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 };
          // si creano due dipendenti
          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 };
          // si creano le aliquote contributive
          Cotisations cotisations = new Cotisations() { Id = 0, CsgRds = 3.49, Csgd = 6.15, Secu = 9.39, Retraite = 7.88 };
          // si salva il tutto
          session.Save(e1);
          session.Save(e2);
          session.Save(cotisations);
          // conferma transazione
          transaction.Commit();
        }
      }
    }

  }
}

Spiegazioni

  • riga 19: viene creato il SessionFactory
  • righe 37-43: viene chiuso nella clausola finally del try
  • riga 55: il metodo ClearDataBase1 che svuota il database. Il principio è il seguente:
    • si recuperano tutti i dipendenti (riga 64) in un elenco
    • li si eliminano uno per uno (righe 67-70)
  • riga 93: il metodo FillDataBase1 inserisce alcuni dati nel database
  • si creano due entità Indemnites (righe 102, 103)
  • si creano due dipendenti con tali indennità (righe 105, 106)
  • si crea un oggetto Cotisations alla riga 108.
  • righe 110, 111: le due entità «Dipendente» vengono salvate nel database
  • riga 112: a sua volta, l’entità «Cotizzazioni» viene salvata
  • potrebbe sorprendere il fatto che le entità Indemnités delle righe 102 e 103 non siano state salvate. In realtà sono state salvate contemporaneamente alle entità Employe. Per capirlo, occorre tornare al mapping dell’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 riga 15, che mappa la relazione di chiave esterna dall’entità Employe all’entità Indemnites, presenta l’attributo cascade="save-update", il che comporta che le operazioni "save " e " update " dell'entità Employe vengano propagate all'entità interna Indemnites.

Schermata ottenuta:

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. Ricerca di un dipendente

Il programma [Program.cs] offre diversi metodi che illustrano l'accesso e la gestione dei dati del database. Ne presentiamo alcuni.

Il metodo [FindEmployee] consente di trovare un dipendente in base al suo numero di previdenza sociale:


// FindEmployee
    static void FindEmployee() {
      try {
        // si avvia la sessione 
        using (ISession session = sessionFactory.OpenSession()) {
          // inizio transazione
          using (ITransaction transaction = session.BeginTransaction()) {
            // ricerca di un dipendente in base al suo numero 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é...");
            }

            // conferma transazione
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Spiegazioni

  • riga 10: la query Select configurata da numSecu da eseguire
  • riga 11: l'assegnazione di un valore al parametro numSecu e l'esecuzione del metodo UniqueResult per ottenere un unico risultato.

Schermata ottenuta:

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. Inserimento di entità non valide

Il metodo seguente tenta di salvare un'entità [Employe] non inizializzata.


// SaveEmptyEmployee
    static void SaveEmptyEmployee() {
      try {
        // accesso 
        using (ISession session = sessionFactory.OpenSession()) {
          // inizio transazione
          using (ITransaction transaction = session.BeginTransaction()) {
            // si crea un dipendente vuoto
            Employe e = new Employe();
            // si crea un'indennità inesistente
            Indemnites i = new Indemnites() { Id = 0, Indice = 3, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
            // che si associa al dipendente
            e.Indemnites = i;
            // si salva il dipendente lasciando vuoti gli altri campi
            session.Save(e);
            // si esegue il commit della transazione
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Spiegazioni

Ricordiamo il codice della classe [Employe]:


namespace PamNHibernateDemos {
    public class Employe {
        // proprietà automatiche
        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; }

        // costruttori
        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 oggetto [Employe] non inizializzato avrà il valore null per tutti i suoi campi di tipo stringa. Durante l'inserimento del record nella tabella [employes], NHibernate lascerà vuote le colonne corrispondenti a tali campi. Tuttavia, nella tabella [employes] tutte le colonne hanno l’attributo not null, il che impedisce la presenza di colonne prive di valore. Il driver ADO.NET genererà quindi un’eccezione:

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. Creazione di due indennità con lo stesso indice all’interno di una transazione

Nella tabella [indemnites], la colonna [indice] è stata dichiarata con l’attributo «unique», il che impedisce di avere due righe con lo stesso indice. Il metodo seguente crea due indennità con lo stesso indice all’interno di una transazione:


// CreateIndemnites1
    static void CreateIndemnites1() {
      try {
        // accesso 
        using (ISession session = sessionFactory.OpenSession()) {
          // inizio transazione
          using (ITransaction transaction = session.BeginTransaction()) {
            // si creano due indennità con lo stesso 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 };
            // si salvano
            session.Save(i1);
            session.Save(i2);
            // conferma transazione
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Spiegazioni

  • nelle righe 9 e 10 si creano due entità Indemnites con lo stesso indice. Tuttavia, nel database, la colonna INDICE ha l’attributo UNIQUE.
  • Le righe 12 e 13 inseriscono le due entità Indemnites nel contesto di persistenza. Quest’ultimo viene sincronizzato con il database al momento della convalida della transazione (riga 15). Tale sincronizzazione genererà due INSERT. Il secondo genererà un'eccezione a causa dell'unicità della colonna INDICE. Poiché ci si trova all'interno di una transazione, il primo INSERT verrà annullato.

Il risultato ottenuto è il seguente:

Effacement base -------------------------------------
--------------- eliminazione dei dipendenti
--------------- eliminazione delle indennità
--------------- eliminazione delle aliquote contributive
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 -------------------------------------
--------------- elenco dei dipendenti
--------------- elenco delle indennità
--------------- tabella delle aliquote contributive

Alla riga 9 si può notare che la tabella [indemnites] è vuota. Non è stato effettuato alcun inserimento.

6.5.6. Creazione di due indennità con lo stesso indice al di fuori della transazione

Il metodo seguente crea due indennità con lo stesso indice senza utilizzare alcuna transazione:


// CreateIndemnites2
    static void CreateIndemnites2() {
      try {
        // accesso 
        using (ISession session = sessionFactory.OpenSession()) {

          // si creano due indennità con lo stesso 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 };
          // si salvano
          session.Save(i1);
          session.Save(i2);
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Spiegazioni

  • si utilizza lo stesso codice di prima, ma senza transazione.
  • La sincronizzazione del contesto di persistenza con il database avverrà alla chiusura di tale contesto, riga 13 (chiusura di Session). La sincronizzazione genererà due INSERT. Il secondo fallirà a causa dell'unicità della colonna INDICE. Tuttavia, poiché non ci si trova all'interno di una transazione, il primo INSERT non verrà annullato.

Il risultato ottenuto è il seguente:

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 -------------------------------------
--------------- elenco dei dipendenti
--------------- elenco delle indennità
[1|1,93|2|3|12]
--------------- tabella dei tassi contributivi

Il database era vuoto prima dell’esecuzione del metodo. Alla riga 6 si può notare che la tabella [indemnites] contiene una riga.