Skip to content

6. Introdução ao ORM NHibernate

Este capítulo é uma breve introdução ao NHibernate, o equivalente .NET do framework Java Hibernate. Para uma introdução abrangente, consulte:


Título: NHibernate in Action, Autor: Pierre-Henri Kuaté, Editora: Manning, ISBN-13: 978-1932394924


Um ORM (Object Relational Mapper) é um conjunto de bibliotecas que permite que um programa que utiliza uma base de dados opere sobre ela sem emitir comandos SQL explícitos e sem conhecer os detalhes do SGBD que está a ser utilizado.


Pré-requisitos


Numa escala de [iniciante-intermédio-avançado], este documento enquadra-se na categoria [intermédio]. A sua compreensão requer vários pré-requisitos que podem ser encontrados em alguns dos documentos que escrevi:

  1. C# 2008: [Aprendendo C# Versão 3.0 com o .NET 3.5 Framework]
  2. [Spring IoC], disponível no URL [Spring IoC para .NET]. Apresenta os conceitos básicos de Inversão de Controlo (IoC) ou Injeção de Dependências (DI) na estrutura Spring.NET [Spring.NET | Página inicial].

Recomendações de leitura são, por vezes, fornecidas no início dos parágrafos deste documento. Estas remetem para os documentos anteriores.


Ferramentas


As ferramentas utilizadas neste estudo de caso estão disponíveis gratuitamente na Internet. São as seguintes (dezembro de 2011):

  • Nhibernate 3.2 disponível em [http://nhforge.org/Default.aspx]
  • Spring.net 1.3.2 disponível no URL [http://www.springframework.net]. O framework Spring.net é muito abrangente. Aqui, utilizaremos apenas a biblioteca que ele fornece para facilitar a utilização do framework Nhibernate.
  • Log4net 1.2.10 disponível em [http://logging.apache.org/log4net]. Esta estrutura de registo é utilizada pelo Nhibernate.
  • NUnit 2.5, disponível em [http://www.nunit.org/]. Esta estrutura de testes unitários é o equivalente .NET da estrutura JUnit para a plataforma Java.
  • O controlador ADO.NET 6.4.4 para o SGBD MySQL 5 está disponível em [http://dev.mysql.com/downloads/connector/net]

Todas as DLLs necessárias para projetos do Visual Studio 2010 foram compiladas numa pasta [libnet4]:

 

6.1. O papel do NHIBERNATE numa arquitetura .NET em camadas

Uma aplicação .NET que utilize uma base de dados pode ser estruturada em camadas da seguinte forma:

A camada [DAO] comunica com o SGBD através da API ADO.NET (ver secção 3.3). Na arquitetura anterior, o conector [ADO.NET] está ligado ao SGBD. Assim, a classe que implementa a interface [IDbConnection] é:

  • a classe [MySQLConnection] para o SGBD MySQL
  • a classe [SQLConnection] para o SGBD SQLServer

A camada [DAO] depende, portanto, do SGBD utilizado. Certas estruturas (Linq, iBatis.net, NHibernate) eliminam esta restrição adicionando uma camada adicional entre a camada [DAO] e o conector [ADO.NET] do SGBD em uso. Aqui, utilizaremos a estrutura [NHibernate].

No diagrama acima, a camada [DAO] já não comunica com o conector [ADO.NET], mas sim com a estrutura NHibernate, que lhe fornece uma interface independente do conector [ADO.NET] utilizado. Esta arquitetura permite mudar de SGBD sem alterar a camada [DAO]. Apenas o conector [ADO.NET] precisa de ser alterado.

6.2. A base de dados de exemplo

Para demonstrar como trabalhar com o NHibernate, utilizaremos a seguinte base de dados MySQL [dbpam_nhibernate] descrita na Secção 3.1. A exportação da estrutura da base de dados para um ficheiro SQL produz o seguinte resultado:

#
# Structure for the `cotisations` table : 
#

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

#
# Structure for the `indemnites` table : 
#

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

#
# Structure for the `employes` table : 
#

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

Note que nas linhas 6, 20 e 36, as chaves primárias ID têm o atributo *<a id="autoincrement"></a> * definido como *autoincrement*. Isto significa que o MySQL irá gerar automaticamente os valores da chave primária sempre que for adicionado um novo registo. O programador não precisa de se preocupar com isto.

6.3. O projeto de demonstração em C#

Para apresentar a configuração e a utilização do NHibernate, utilizaremos a seguinte arquitetura:

Um programa de consola [1] irá manipular dados da base de dados [2] através da estrutura [NHibernate] [3]. Isto irá levar-nos a apresentar:

  • os ficheiros de configuração do NHibernate
  • a API do NHibernate

O projeto em C# será o seguinte:

Os elementos necessários para o projeto são os seguintes:

  • em [1], as DLLs necessárias para o projeto:
    • [NHibernate]: a DLL do framework NHibernate
    • [MySql.Data]: a DLL para o conector ADO.NET do SGBD MySQL
    • [log4net]: a DLL da estrutura Log4net para a geração de registos
  • em [2], as classes que representam as tabelas da base de dados
  • em [3], o ficheiro [App.config] que configura toda a aplicação, incluindo o framework [NHibernate]
  • em [4], aplicações de consola de teste

6.3.1. Configurar a ligação à base de dados

Voltemos à arquitetura de teste:

Como mostrado acima, o [NHibernate] deve poder aceder à base de dados. Para tal, necessita de determinadas informações:

  • o SGBD que gere a base de dados (MySQL, SQL Server, Postgres, Oracle, etc.). A maioria dos SGBDs adicionou as suas próprias extensões à linguagem SQL. Ao conhecer o SGBD, o NHibernate pode adaptar as instruções SQL que emite a esse SGBD específico. O NHibernate utiliza o conceito de dialetos SQL.
  • os parâmetros de ligação à base de dados (nome da base de dados, nome de utilizador do proprietário da ligação e palavra-passe)

Estas informações podem ser colocadas no ficheiro de configuração [App.config]. Aqui está o que será utilizado com uma base de dados MySQL 5:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <!-- configuration sections -->
    <configSections>
        <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
        <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
    </configSections>
 
 
    <!-- configuration NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
    </hibernate-configuration>
 
    <!-- This section contains the log4net configuration settings -->
    <!-- NOTE IMPORTANTE: logs are not active by default. They must be activated programmatically with the log4net.Config.XmlConfigurator.Configure() instruction;
    ! -->
    <log4net>
        <!-- Define an output appender (where the logs can go) -->
        <appender name="LogFileAppender" type="log4net.Appender.FileAppender, log4net">
            <param name="File" value="log.txt" />
            <param name="AppendToFile" value="false" />
            <layout type="log4net.Layout.PatternLayout, log4net">
                <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] &lt;%X{auth}&gt; - %m%n" />
            </layout>
        </appender>
        <appender name="LogDebugAppender" type="log4net.Appender.DebugAppender, log4net">
            <layout type="log4net.Layout.PatternLayout, log4net">
                <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] &lt;%X{auth}&gt; - %m%n"/>
            </layout>
        </appender>
        <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender, log4net">
            <layout type="log4net.Layout.PatternLayout, log4net">
                <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] &lt;%X{auth}&gt; - %m%n"/>
            </layout>
        </appender>
 
        <!-- Setup the root category, set the default priority level and add the appender(s) (where the logs will go) -->
        <root>
            <priority value="INFO" />
            <!--
            <appender-ref ref="LogFileAppender" />
            <appender-ref ref="LogDebugAppender"/>
            -->
            <appender-ref ref="ConsoleAppender"/>
        </root>
 
        <!-- Specify the level for some specific namespaces -->
        <!-- Level can be : ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF -->
        <logger name="NHibernate">
            <level value="INFO" />
        </logger>
    </log4net>
</configuration>
  • Linhas 4–7: definem secções de configuração no ficheiro [App.config]. Considere a linha 6:

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

Esta linha define a secção de configuração do NHibernate no ficheiro [App.config]. Possui dois atributos: name e type.

  • O atributo [name] nomeia a secção de configuração. Esta secção deve ser delimitada pelas tags <name>...</name>, neste caso <hibernate-configuration>...</hibernate-configuration> nas linhas 11–24.
  • O atributo [type=class,DLL] especifica o nome da classe responsável por gerir a secção definida pelo atributo [name], bem como a DLL que contém essa classe. Aqui, a classe chama-se [NHibernate.Cfg.ConfigurationSectionHandler] e está localizada na DLL [NHibernate.dll]. Recorde-se que esta DLL é uma das referências do projeto.

Agora, vamos analisar a secção de configuração do NHibernate:


    <!-- configuration NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>
  • Linha 2: A configuração do NHibernate está contida na tag <hibernate-configuration>. O atributo xmlns (XML Namespace) especifica a versão utilizada para configurar o NHibernate. Ao longo do tempo, a forma como o NHibernate é configurado evoluiu. Aqui, é utilizada a versão 2.2.
  • Linha 3: Toda a configuração do NHibernate está contida na tag <session-factory> (linhas 3 e 14). Uma sessão do NHibernate é a ferramenta utilizada para trabalhar com uma base de dados de acordo com o esquema:
    • abrir sessão
    • trabalhar com a base de dados utilizando métodos da API do NHibernate
    • fechar sessão

A sessão é criada por uma fábrica, um termo genérico que se refere a uma classe capaz de criar objetos. As linhas 3–14 configuram esta fábrica.

  • Linhas 4, 6, 8, 9: configuram a ligação à base de dados de destino. As principais informações incluem o nome do SGBD utilizado, o nome da base de dados, o ID do utilizador e a palavra-passe.
  • Linha 4: define o fornecedor de ligação, a entidade à qual é solicitada uma ligação à base de dados. O valor da propriedade [connection.provider] é o nome de uma classe NHibernate. Esta propriedade é independente do SGBD utilizado.
  • Linha 6: O controlador ADO.NET a utilizar. Este é o nome de uma classe NHibernate especializada para um determinado SGBD, neste caso o MySQL. A linha 6 foi comentada porque não é essencial.
  • Linha 8: A propriedade [dialect] define o dialeto SQL a ser utilizado com o SGBD. Aqui, trata-se do dialeto do SGBD MySQL.

Se mudar de SGBD, como encontra o dialeto NHibernate correspondente? Volte ao projeto C# anterior e clique duas vezes na DLL [NHibernate] no separador [Referências]:

  • Em [1], o separador [Object Explorer] apresenta várias DLLs, incluindo as referenciadas pelo projeto.
  • Em [2], a DLL [NHibernate]
  • Em [3], a DLL [NHibernate]. Aqui pode ver os vários namespaces definidos no seu interior.
  • em [4], o namespace [NHibernate.Dialect], onde se encontram as classes que definem os vários dialetos SQL utilizáveis.
  • Em [5], a classe para o dialeto do SGBD MySQL 5.
  • em [6], o namespace da classe [MySqlDataDriver] utilizada na linha 6 abaixo:

    <!-- configuration NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQLDialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>
  • Linhas 9–11: a cadeia de ligação à base de dados. Esta cadeia tem o formato "param1=val1;param2=val2; ...". O conjunto de parâmetros definido desta forma permite que o controlador do SGBD estabeleça uma ligação. O formato desta cadeia de ligação depende do SGBD utilizado. As cadeias de ligação para os principais SGBDs podem ser encontradas no site [http://www.connectionstrings.com/]. Aqui, a cadeia "Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;" é uma cadeia de ligação para o SGBD MySQL. Indica que:
    • Server=localhost; : o DBMS está na mesma máquina que o cliente que tenta abrir a ligação
    • Database=dbpam_nhibernate; : a base de dados MySQL de destino
    • Uid=root; : o utilizador que abre a ligação é o utilizador root
    • Pwd=; : este utilizador não tem palavra-passe (um caso especial neste exemplo)
  • Linha 12: A propriedade [show_sql] especifica se o NHibernate deve exibir as instruções SQL que emite para a base de dados nos seus registos. Durante o desenvolvimento, é útil definir esta propriedade como [true] para ver exatamente o que o NHibernate está a fazer.
  • linha 13: para compreender a tag <mapping>, vamos rever a arquitetura da aplicação:

Se o programa de consola fosse um cliente direto do conector ADO.NET e quisesse a lista de funcionários, faria com que o conector executasse uma instrução SQL Select e receberia em troca um objeto do tipo IDataReader que teria de processar para obter a lista de funcionários inicialmente desejada.

No exemplo acima, o programa de consola é o cliente do NHibernate, e o NHibernate é o cliente do conector ADO.NET. Veremos mais tarde que a API do NHibernate permitirá que o programa de consola solicite a lista de funcionários. O NHibernate traduzirá esta solicitação numa instrução SQL Select que fará com que o conector ADO.NET a execute. O conector irá devolver um objeto do tipo IDataReader. A partir deste objeto, o NHibernate deve ser capaz de construir a lista de funcionários que foi solicitada. Isto é possível através da configuração. Cada tabela na base de dados está associada a uma classe C#. Assim, com base nas linhas da tabela [employees] devolvidas pelo IDataReader, o NHibernate será capaz de construir uma lista de objetos que representam os funcionários e devolvê-la ao programa de consola. Estes mapeamentos de tabela para classe são definidos em ficheiros de configuração. O NHibernate utiliza o termo «mapeamento» para descrever estas relações.

Voltemos à linha 13 abaixo:


    <!-- configuration NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>

A linha 13 indica que os ficheiros de configuração de mapeamento de tabela para classe se encontram no assembly [pam-nhibernate-demos]. Um assembly é o executável ou DLL produzido pela compilação de um projeto. Aqui, os ficheiros de mapeamento serão colocados no assembly do projeto de exemplo. Para encontrar o nome deste assembly, verifique as propriedades do projeto:

  • em [1], as propriedades do projeto
  • no separador [Aplicação] [2], o nome do assembly [3] que será gerado.
  • Como o tipo de saída é [Aplicação de Consola] [4], o ficheiro gerado quando o projeto for compilado terá o nome [pam-nhibernate-demos.exe]. Se o tipo de saída fosse [Biblioteca de Classes] [5], o ficheiro gerado quando o projeto fosse compilado teria o nome [pam-nhibernate-demos.dll]
  • O assembly é gerado na pasta [bin/Release] do projeto [6].

A partir da explicação anterior, verificamos que a tabela de mapeamento <--> ficheiros de classe deve ser incluída no ficheiro [pam-nhibernate-demos.exe] [6].

6.3.2. Configurar a tabela de mapeamento <-->classe

Voltemos à arquitetura do projeto em estudo:

  • Em [1], o programa de consola utiliza os métodos da API do framework NHibernate. Estes dois blocos trocam objetos.
  • Em [2], o NHibernate utiliza a API de um conector .NET. Envia comandos SQL para o SGBD de destino.

O programa de consola irá manipular objetos que refletem as tabelas da base de dados. Neste projeto, esses objetos e as ligações que os conectam às tabelas da base de dados foram colocados na pasta [Entities] abaixo:

 
  • Cada tabela da base de dados corresponde a uma classe e a um ficheiro de mapeamento entre as duas
Tabela
Classe
Mapeamento
contribuições
MembershipFees.cs
Contributions.hbm.xml
funcionários
Employee.cs
Employee.hbm.xml
subsídios
Subsídios.cs
Subsídios.hbm.xml

6.3.2.1. Mapeamento da tabela [contributions]

Considere a tabela [contributions]:

ID
chave primária auto-incrementada
VERSÃO
registar o número da versão
SECU
taxa de contribuição para a segurança social (percentagem)
PENSÃO
taxa de contribuição para a pensão
CSGD
taxa de contribuição para a contribuição social geral dedutível
CSGRDS
taxa de contribuição para a contribuição social geral e a contribuição para o pagamento da dívida social

Uma linha desta tabela pode ser encapsulada num objeto do tipo [ Cotisations.cs] da seguinte forma:


namespace PamNHibernateDemos {
    public class Cotisations {
        // automatic properties
        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; }
 
        // manufacturers
        public Cotisations() {
        }
        // ToString
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}]", CsgRds, Csgd, Secu, Retraite);
        }
    }
 
}

Foi criada uma propriedade gerada automaticamente para cada coluna da tabela [contributions]. Cada uma destas propriedades deve ser declarada como virtual, porque o NHibernate irá derivar a classe e substituir as suas propriedades. Por conseguinte, estas propriedades devem ser virtuais.

Note, na linha 1, que a classe pertence ao namespace [PamNHibernateDemos].

O ficheiro de mapeamento [Cotisations.hbm.xml] entre a tabela [contributions] e a classe [Cotisations] é o seguinte:


<?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>
  • O ficheiro de mapeamento é um ficheiro XML definido dentro da tag <hibernate-mapping> (linhas 2 e 14)
  • Linha 4: A tag <class> associa uma tabela da base de dados a uma classe. Aqui, a tabela [COTISATIONS] (atributo table) e a classe [Cotisations] (atributo name). No .NET, uma classe deve ser definida pelo seu nome totalmente qualificado (incluindo o namespace) e pelo assembly que a contém. Estas duas informações são fornecidas pela linha 3. A primeira (namespace) pode ser encontrada na definição da classe. A segunda (assembly) é o nome do assembly do projeto. Já explicámos como encontrar este nome.
  • Linhas 5–7: A tag <id> é utilizada para definir o mapeamento da chave primária da tabela [contributions].
    • Linha 5: O atributo name designa o campo na classe [Cotisations] que irá conter a chave primária da tabela [cotisations]. O atributo column designa a coluna na tabela [cotisations] que serve como chave primária. O atributo unsaved-value é utilizado para definir uma chave primária que ainda não foi gerada. Este valor permite ao NHibernate saber como guardar um objeto [Cotisations] na tabela [cotisations]. Se este objeto tiver um campo Id com o valor 0, será executada uma operação SQL INSERT; caso contrário, será executada uma operação SQL UPDATE. O valor de unsaved-value depende do tipo do campo Id na classe [Cotisations]. Aqui, é do tipo int, e o valor por defeito para um tipo int é 0. Um objeto [Cotisations] que ainda não tenha sido guardado (e, portanto, não tenha chave primária) terá, assim, o seu campo Id definido como 0. Se o campo Id fosse do tipo Object ou de um tipo derivado, teríamos escrito unsaved-value=null.
    • Linha 6: Quando o NHibernate precisa de guardar um objeto [Cotisations] com o campo Id definido como 0, deve realizar uma operação INSERT na base de dados, durante a qual deve obter um valor para a chave primária do registo. A maioria dos SGBDs possui um método proprietário para gerar automaticamente este valor. A tag <generator> é utilizada para definir o mecanismo a ser utilizado para gerar a chave primária. A tag <generator class="native"> indica que o mecanismo padrão do SGBD em uso deve ser empregado. Vimos na Secção 6.2 que as chaves primárias das nossas três tabelas MySQL tinham o atributo autoincrement. Durante as suas operações INSERT, o NHibernate não fornecerá um valor para a coluna ID do registo adicionado, permitindo que o MySQL gere esse valor.
  • Linha 8: A tag <version> é utilizada para definir a coluna da tabela (bem como o campo de classe correspondente) que permite que os registos sejam «versionados». Inicialmente, a versão é definida como 1. É incrementada a cada operação UPDATE. Além disso, todas as operações UPDATE ou DELETE são realizadas com uma cláusula WHERE: ID=id AND VERSION=v1. Um utilizador só pode, portanto, modificar ou eliminar um objeto se tiver a versão correta do mesmo. Se não for esse o caso, o NHibernate lança uma exceção.
  • Linha 9: A tag <property> é utilizada para definir um mapeamento de coluna normal (nem uma chave primária nem uma coluna de versão). Assim, a linha 9 indica que a coluna CSGRDS da tabela [COTISATIONS] está associada à propriedade CsgRds da classe [Cotisations].

6.3.2.2. Mapeamento da tabela [indemnites]

Considere a tabela [indemnites]:

ID
chave primária do tipo autoincrement
VERSÃO
Número da versão do registo
BASE_HOUR
Custo em euros por uma hora de serviço de plantão
DIÁRIA_DIÁRIA
subsídio diário em euros
REFEIÇÃO_DIÁRIA
Subsídio de refeição em euros por dia de assistência
PAID_LEAVE_ALLOWANCE
Subsídios de férias pagas. Trata-se de uma percentagem a aplicar ao salário base.

Uma linha desta tabela pode ser encapsulada num objeto do tipo [ Indemnites] da seguinte forma:


namespace PamNHibernateDemos {
    public class Indemnites {
 
        // automatic properties
        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; }
 
        // manufacturers
        public Indemnites() {
        }
 
        // identity
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}|{4}]", Indice, BaseHeure, EntretienJour, RepasJour, IndemnitesCp);
        }
 
    }
}

O ficheiro de mapeamento para a tabela [indemnites] <--> classe [Indemnites] poderia ser o seguinte (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>

Não há nada de novo aqui em comparação com o ficheiro de mapeamento explicado anteriormente. A única diferença está na linha 9. O atributo unique="true" indica que existe uma restrição de unicidade na coluna [INDICE] da tabela [indemnites]: não podem existir duas linhas com o mesmo valor na coluna [INDICE].

6.3.2.3. Mapeamento da tabela [employes]

Vamos considerar a tabela [employes]:

ID
chave primária do tipo autoincrement
VERSÃO
número da versão do registo
FIRST_NAME
primeiro nome do funcionário
APELIDO
apelido
ENDEREÇO
a morada deles
Código postal
o seu código postal
CIDADE
a sua cidade
INDEMNITY_ID
chave estrangeira em INDEMNITIES(ID)

A diferença em relação às tabelas anteriores é a presença de uma chave estrangeira: a coluna [INDEMNITE_ID] é uma chave estrangeira na coluna [ID] da tabela [INDEMNITES]. Este campo faz referência à linha na tabela [INDEMNITES] a ser utilizada para calcular os subsídios do funcionário.

A classe [ Employe] que representa a tabela [employes] poderia ser a seguinte:


namespace PamNHibernateDemos {
    public class Employe {
        // automatic properties
        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; }
 
        // manufacturers
        public Employe() {
        }
 
        // ToString
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
        }
    }
}

O ficheiro de mapeamento [Employee.hbm.xml] pode ter o seguinte aspeto:


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

A nova funcionalidade encontra-se na linha 15, com a introdução de uma nova tag: <many-to-one>. Esta tag é utilizada para mapear uma coluna de chave estrangeira [INDEMNITE_ID] da tabela [EMPLOYEES] para a propriedade [Benefits] da classe [Employee]:


namespace PamNHibernateDemos {
    public class Employe {
        // automatic properties
..
        public virtual Indemnites Indemnites { get; set; }
 
...
    }
}

A tabela [EMPLOYEES] possui uma chave estrangeira [BENEFIT_ID] que faz referência à coluna [ID] na tabela [BENEFITS]. Várias (muitas) linhas na tabela [EMPLOYEES] podem fazer referência a uma única (uma) linha na tabela [BENEFITS]. Daí o nome da tag <many-to-one>. Esta tag possui os seguintes atributos aqui:

  • column: especifica o nome da coluna na tabela [EMPLOYEES] que serve como chave estrangeira na tabela [BENEFITS]
  • name: especifica a propriedade da classe [Employee] associada a esta coluna. O tipo desta propriedade deve ser a classe associada à tabela de destino da chave estrangeira, neste caso a tabela [Compensation]. Sabemos que esta classe é a classe [Indemnites] já descrita. Isto reflete-se na linha 5 acima. Isto significa que quando o NHibernate recupera um objeto [Employee] da base de dados, também recuperará o objeto [Indemnites] correspondente.
  • cascade: este atributo pode ter vários valores:
    • save-update: uma operação de inserção (save) ou atualização no objeto [Employee] deve ser propagada para o objeto [Benefits] que este contém.
    • delete: a eliminação de um objeto [Employee] deve propagar-se para o objeto [Benefits] que este contém.
    • all: propaga operações de inserção (guardar), atualização e eliminação.
    • none: não propaga nada

Por fim, vamos rever a configuração do NHibernate no ficheiro [App.config]:


    <!-- configuration NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>

A linha 13 especifica que os ficheiros de mapeamento *.hbm.xml serão encontrados no assembly [pam-nhibernate-demos]. Este não é o comportamento padrão. Deve configurá-lo no projeto C#:

  • Em [1], selecione as propriedades de um ficheiro de mapeamento
  • em [2], a ação de geração deve ser [Recurso Incorporado] [3]. Isto significa que, quando o projeto for gerado, o ficheiro de mapeamento deve ser incorporado no assembly gerado.

6.4. A API do NHibernate

Voltemos à arquitetura do nosso projeto de exemplo:

Nas secções anteriores, configurámos o NHibernate de duas formas:

  • No [App.config], configurámos a ligação à base de dados
  • para cada tabela na base de dados, escrevemos a classe que representa essa tabela e o ficheiro de mapeamento que nos permite converter entre a classe e a tabela e vice-versa.

Ainda precisamos de explorar os métodos oferecidos pelo NHibernate para manipular dados da base de dados: inserir, atualizar, eliminar e listar.

6.4.1. O objeto SessionFactory

Todas as operações do NHibernate ocorrem dentro de uma sessão. Uma sequência típica de operações do NHibernate é a seguinte:

  • abrir uma sessão do NHibernate
  • iniciar uma transação dentro da sessão
  • executar operações de persistência com a sessão (Load, Get, Find, CreateQuery, Save, SaveOrUpdate, Delete)
  • confirmar ou reverter a transação
  • fechar a sessão do NHibernate

Uma sessão é obtida a partir de uma fábrica [SessionFactory]. Esta fábrica é aquela configurada pela tag <session-factory> no ficheiro de configuração [App.config]:


    <!-- configuration NHibernate -->
    <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
        <session-factory>
            <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
            <!--
            <property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property>
            -->
            <property name="dialect">NHibernate.Dialect.MySQL5Dialect</property>
            <property name="connection.connection_string">
                Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;
            </property>
            <property name="show_sql">false</property>
            <mapping assembly="pam-nhibernate-demos"/>
        </session-factory>
</hibernate-configuration>

No código C#, o SessionFactory pode ser obtido da seguinte forma:


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

A classe Configuration é uma classe do framework NHibernate. A instrução anterior utiliza a secção de configuração do NHibernate no ficheiro [App.config]. O objeto [ISessionFactory] resultante possui então as:

  • informações necessárias para criar uma ligação à base de dados de destino
  • ficheiros de mapeamento entre tabelas de base de dados e classes persistentes geridas pelo NHibernate.

6.4.2. A sessão do NHibernate

Depois de criada a SessionFactory (isto é feito apenas uma vez), pode obter sessões que lhe permitem realizar operações de persistência do NHibernate. Um trecho de código comum é o seguinte:


try{
      // session opening 
      using (ISession session = sessionFactory.OpenSession())
      {
        // start of transaction
        using (ITransaction transaction = session.BeginTransaction())
        {
........................ opérations de persistance
          // transaction validation
          transaction.Commit();
        }
      }
}catch (Exception ex){
....
}
  • Linha 3: É criada uma sessão a partir da SessionFactory dentro de um bloco `using`. Quando o bloco `using` terminar, a sessão será automaticamente encerrada. Sem o bloco `using`, a sessão teria de ser encerrada explicitamente (session.Close()).
  • Linha 6: As operações de persistência serão realizadas dentro de uma transação. Ou todas elas são bem-sucedidas, ou nenhuma delas é bem-sucedida. Dentro do bloco `using`, a transação é confirmada com um `Commit` (linha 10). Se uma operação de persistência dentro da transação lançar uma exceção, a transação será automaticamente revertida ao sair do bloco `using`.
  • Os blocos try/catch nas linhas 1 e 13 permitem a interceção de qualquer exceção lançada pelo código dentro do bloco try (sessão, transação, persistência).

6.4.3. A Interface ISession

Apresentaremos agora alguns dos métodos da interface ISession implementados por uma sessão NHibernate:

ITransaction BeginTransaction()
inicia uma transação na sessão
ITransaction tx = session.BeginTransaction();
void Clear()
limpa a sessão. Os objetos nela contidos são desanexados.
session.Clear();
void Close()
Encerra a sessão. Os objetos nela contidos são sincronizados com a base de dados. Esta operação de sincronização também é realizada no final de uma transação. Este último caso é o mais comum.
session.Close();
IQuery CreateQuery(string queryString)
cria uma consulta HQL (Hibernate Query Language) para execução posterior.
IQuery query = session.createQuery("select e from Employee e");
void Delete(object obj)
elimina um objeto. Este objeto pode pertencer à sessão (associado) ou não (não associado). Quando a sessão estiver sincronizada com a base de dados, será executada uma operação SQL DELETE neste objeto.
// carrega um funcionário da base de dados
Employee e = session.Get<Employee>(143);
// eliminá-lo
session.Delete(e);
void Flush()
Força a sessão a sincronizar-se com a base de dados. O conteúdo da sessão não é alterado.
session.Flush();
T Get<T>(object id)
recupera o objeto T com a chave primária id da base de dados. Se este objeto não existir, devolve um ponteiro nulo.
// Carregar um funcionário da base de dados
Employee e = session.Get<Employee>(143);
object Save(object obj)
adiciona o objeto obj à sessão. Este objeto não tem chave primária antes do Save. Após o Save, passa a ter uma. Quando a sessão é sincronizada, será executada uma operação SQL INSERT na base de dados.
// Criar um funcionário
Employee e = new Employee(){...};
// guardá-lo
e = session.Save(e);
SaveOrUpdate(object obj)
Executa uma operação de gravação se obj não tiver uma chave primária, ou uma operação de atualização se já tiver uma.
void Update(object obj)
atualiza o objeto obj na base de dados. Em seguida, é executada uma operação SQL UPDATE na base de dados.
// Carregar um funcionário da base de dados
Employee e = session.Get<Employee>(143);
// alterar o nome
e.Name = ...;
// Atualizar o funcionário na base de dados
session.Update(e);

6.4.4. A interface IQuery

A interface IQuery permite-lhe consultar a base de dados para extrair dados. Já vimos como criar uma instância da mesma:

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

O parâmetro do método createQuery é uma consulta HQL (Hibernate Query Language), uma linguagem semelhante ao SQL, mas que consulta classes em vez de tabelas. A consulta acima recupera uma lista de todos os funcionários. Aqui estão alguns exemplos de consultas 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

Vamos agora apresentar alguns dos métodos da interface IQuery:

IList<T> List<T>()
retorna o resultado da consulta como uma lista de objetos T
IList<Employee> employees = session.createQuery("select e from Employee e order by e.Name asc").List<Employee>();
IList List()
Retorna o resultado da consulta como uma lista, em que cada item da lista representa uma linha da consulta SELECT na forma de uma matriz de objetos.
IList rows = session.createQuery("select e.LastName, e.FirstName, e.SocialSecurity from Employee e").List();
lines[i][j] representa a coluna j da linha i como um objeto. Assim, lines[10][1] é um objeto que representa o primeiro nome de uma pessoa. Geralmente, é necessário realizar a conversão de tipos para recuperar os dados no seu tipo exato.
T UniqueResult<T>()
retorna o primeiro objeto do resultado da consulta
Employee e = session.createQuery("select e from Employee e where e.LastName='MARTIN'").UniqueResult<Employee>();

Uma consulta HQL pode ser parametrizada:

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

Na consulta HQL na linha 3, :num é um parâmetro ao qual deve ser atribuído um valor antes da consulta ser executada. Acima, o método SetString é utilizado para este fim. A interface IQuery fornece vários métodos Set para atribuir um valor a um parâmetro:

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

6.5. Alguns exemplos de código

Os exemplos seguintes baseiam-se na arquitetura discutida anteriormente e resumida abaixo. A base de dados é a base de dados MySQL [dbpam_nhibernate] também apresentada. Os exemplos são programas de consola [1] que utilizam o framework NHibernate [3] para manipular a base de dados [2].

O projeto em C# no qual os exemplos a seguir estão incorporados é o que já foi apresentado:

  • em [1], as DLLs necessárias para o projeto:
    • [NHibernate]: a DLL do framework NHibernate
    • [MySql.Data]: a DLL do conector ADO.NET para o SGBD MySQL 5
    • [log4net]: a DLL para uma ferramenta de registo
  • em [2], as classes que representam as tabelas da base de dados
  • em [3], o ficheiro [App.config] que configura toda a aplicação, incluindo a estrutura [NHibernate]
  • em [4], aplicações de consola de teste. São estas que iremos apresentar em parte.

6.5.1. Recuperação do conteúdo da base de dados

O programa [ShowDataBase.cs] exibe o conteúdo da base de dados:


using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;
 
 
namespace PamNHibernateDemos
{
  public class ShowDataBase
  {
 
    private static ISessionFactory sessionFactory = null;
 
    // main program
    static void Main(string[] args)
    {
      // factory initialization NHibernate
      sessionFactory = new Configuration().Configure().BuildSessionFactory();
      try
      {
        // database content display
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase1();
      }
      catch (Exception ex)
      {
        // exception is displayed 
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
      }
      finally
      {
        if (sessionFactory != null)
        {
          sessionFactory.Close();
        }
      }
      // keyboard wait
      Console.ReadLine();
    }
 
    // test1
    static void ShowDataBase1()
    {
      // session opening 
      using (ISession session = sessionFactory.OpenSession())
      {
        // start of transaction
        using (ITransaction transaction = session.BeginTransaction())
        {
          // retrieve the list of employees
          IList<Employe> employes = session.CreateQuery(@"select e from Employe e order by e.Nom asc").List<Employe>();
          // we display it
          Console.WriteLine("--------------- liste des employés");
          foreach (Employe e in employes)
          {
            Console.WriteLine(e);
          }
          // retrieve the list of benefits
          IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i order by i.Indice asc").List<Indemnites>();
          // we display it
          Console.WriteLine("--------------- liste des indemnités");
          foreach (Indemnites i in indemnites)
          {
            Console.WriteLine(i);
          }
          // retrieve the list of contributions
          Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
          Console.WriteLine("--------------- tableau des taux de cotisations");
          Console.WriteLine(cotisations);
          // commit transaction
          transaction.Commit();
        }
      }
    }
  }
}

Explicações:

  • linha 19: o objeto SessionFactory é criado. É isto que nos permitirá obter objetos Session.
  • linha 24: o conteúdo da base de dados é apresentado
  • Linhas 31–37: A SessionFactory é encerrada na cláusula finally do bloco try.
  • Linha 43: o método que exibe o conteúdo da base de dados
  • Linha 46: Obtemos uma Session a partir da SessionFactory.
  • Linha 49: É iniciada uma transação
  • linha 52: consulta HQL para recuperar a lista de funcionários. Devido à chave estrangeira que liga a entidade Employee à entidade Compensation, cada funcionário terá a sua remuneração.
  • Linha 60: consulta HQL para recuperar a lista de subsídios.
  • Linha 68: consulta HQL para recuperar a única linha da tabela de contribuições.
  • Linha 72: Fim da transação
  • Linha 73: Fim do `using ITransaction` da linha 49 – a transação é encerrada automaticamente
  • Linha 74: Fim do `using Isession` da linha 46 – a sessão é encerrada automaticamente.

Exibição no ecrã obtida:

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]

Note-se que nas linhas 3 e 4, ao consultar um funcionário, a sua remuneração também foi apresentada.

6.5.2. Inserção de dados na base de dados

O programa [FillDataBase.cs] permite inserir dados na base de dados:


using System;
using System.Collections;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;
 
 
namespace PamNHibernateDemos
{
  public class FillDataBase
  {
 
    private static ISessionFactory sessionFactory = null;
 
    // main program
    static void Main(string[] args)
    {
      // factory initialization NHibernate
      sessionFactory = new Configuration().Configure().BuildSessionFactory();
      try
      {
        // delete database contents
        Console.WriteLine("Effacement base -------------------------------------");
        ClearDataBase1();
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase();
        Console.WriteLine("Remplissage base -------------------------------------");
        FillDataBase1();
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase();
      }
      catch (Exception ex)
      {
        // exception is displayed 
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
      }
      finally
      {
        if (sessionFactory != null)
        {
          sessionFactory.Close();
        }
      }
      // keyboard wait
      Console.ReadLine();
    }
 
    // test1
    static void ShowDataBase()
    {
             // see previous example
    }
 
    // ClearDataBase1
    static void ClearDataBase1()
    {
      // session opening 
      using (ISession session = sessionFactory.OpenSession())
      {
        // start of transaction
        using (ITransaction transaction = session.BeginTransaction())
        {
          // retrieve the list of employees
          IList<Employe> employes = session.CreateQuery(@"select e from Employe e").List<Employe>();
          // we cut all employees
          Console.WriteLine("--------------- suppression des employés associés");
          foreach (Employe e in employes)
          {
            session.Delete(e);
          }
          // retrieve the list of allowances
          IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i").List<Indemnites>();
          // we do away with allowances
          Console.WriteLine("--------------- suppression des indemnités");
          foreach (Indemnites i in indemnites)
          {
            session.Delete(i);
          }
          // retrieve the list of contributions
          Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
          Console.WriteLine("--------------- suppression des taux de cotisations");
          if (cotisations != null)
          {
            session.Delete(cotisations);
          }
          // commit transaction
          transaction.Commit();
        }
      }
    }
 
    // FillDataBase
    static void FillDataBase1()
    {
      // session opening 
      using (ISession session = sessionFactory.OpenSession())
      {
        // start of transaction
        using (ITransaction transaction = session.BeginTransaction())
        {
          // two allowances are created
          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 };
          // we create two employees
          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 };
          // we create the contribution rates
          Cotisations cotisations = new Cotisations() { Id = 0, CsgRds = 3.49, Csgd = 6.15, Secu = 9.39, Retraite = 7.88 };
          // save it all
          session.Save(e1);
          session.Save(e2);
          session.Save(cotisations);
          // commit transaction
          transaction.Commit();
        }
      }
    }
 
  }
}

Explicações

  • linha 19: o SessionFactory é criado
  • linhas 37–43: é fechada na cláusula finally do bloco try
  • linha 55: o método ClearDataBase1, que limpa a base de dados. O processo é o seguinte:
    • todos os funcionários são recuperados (linha 64) para uma lista
    • eliminamo-los um a um (linhas 67–70)
  • Linha 93: O método FillDataBase1 insere alguns dados na base de dados
  • criamos duas entidades Indemnites (linhas 102, 103)
  • criamos dois funcionários com estes subsídios (linhas 105, 106)
  • Criamos um objeto Cotisations na linha 108.
  • Linhas 110, 111: As duas entidades Employee são persistidas na base de dados
  • linha 112: a entidade Cotisations é, por sua vez, persistida
  • Pode parecer surpreendente que as entidades Allowances nas linhas 102 e 103 não tenham sido persistidas. Na verdade, foram persistidas ao mesmo tempo que as entidades Employee. Para compreender isto, precisamos de analisar o mapeamento da entidade Employee:

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

A linha 15, que mapeia a relação de chave estrangeira entre a entidade Employee e a entidade Allowances, possui o atributo cascade="save-update", o que significa que as operações "save" e "update" na entidade Employee são propagadas para a entidade interna Allowances.

Exibição do ecrã obtida:

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. Pesquisar um funcionário

O programa [Program.cs] contém vários métodos que demonstram como aceder e manipular dados na base de dados. Apresentamos aqui alguns deles.

O método [FindEmployee] permite-lhe encontrar um funcionário pelo seu número de segurança social:


// FindEmployee
    static void FindEmployee() {
      try {
        // session opening 
        using (ISession session = sessionFactory.OpenSession()) {
          // start of transaction
          using (ITransaction transaction = session.BeginTransaction()) {
            // search for an employee using his SS number
            String numSecu = "254104940426058";
            IQuery query = session.CreateQuery(@"select e from Employe e where e.SS=:numSecu");
            Employe employe = query.SetString("numSecu", numSecu).UniqueResult<Employe>();
            if (employe != null) {
              Console.WriteLine("Employe[" + numSecu + "]=" + employe);
            } else {
              Console.WriteLine("Employe[" + numSecu + "] non trouvé...");
            }
 
            numSecu = "xx";
            employe = query.SetString("numSecu", numSecu).UniqueResult<Employe>();
            if (employe != null) {
              Console.WriteLine("Employe[" + numSecu + "]=" + employe);
            } else {
              Console.WriteLine("Employe[" + numSecu + "] non trouvé...");
            }
 
            // commit transaction
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Explicações

  • linha 10: a consulta Select configurada pelo parâmetro numSecu para ser executada
  • linha 11: atribuição de um valor ao parâmetro numSecu e execução do método UniqueResult para devolver um único resultado.

Exibição obtida no ecrã:

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. Inserção de entidades inválidas

O método seguinte tenta guardar uma entidade [Employee] não inicializada.


// SaveEmptyEmployee
    static void SaveEmptyEmployee() {
      try {
        // session opening 
        using (ISession session = sessionFactory.OpenSession()) {
          // start of transaction
          using (ITransaction transaction = session.BeginTransaction()) {
            // create an empty employee
            Employe e = new Employe();
            // we create a non-existent indemnity
            Indemnites i = new Indemnites() { Id = 0, Indice = 3, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
            // associated with the employee
            e.Indemnites = i;
            // save the employee, leaving the other fields empty
            session.Save(e);
            // commit transaction
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Explicações

Vamos rever o código da classe [Employee]:


namespace PamNHibernateDemos {
    public class Employe {
        // automatic properties
        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; }
 
        // manufacturers
        public Employe() {
        }
 
        // ToString
        public override string ToString() {
            return string.Format("[{0}|{1}|{2}|{3}|{4}|{5}|{6}]", SS, Nom, Prenom, Adresse, Ville, CodePostal, Indemnites);
        }
    }
}

Um objeto [Employee] não inicializado terá um valor nulo em todos os seus campos de cadeia de caracteres. Ao inserir o registo na tabela [employees], o NHibernate deixará vazias as colunas correspondentes a esses campos. No entanto, na tabela [employees], todas as colunas têm o atributo not null, o que impede que as colunas fiquem sem valor. O controlador ADO.NET lançará então uma exceção:

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. Criação de dois subsídios com o mesmo índice numa transação

Na tabela [indemnizações], a coluna [índice] foi declarada com o atributo único, o que impede que duas linhas tenham o mesmo índice. O método seguinte cria duas indemnizações com o mesmo índice dentro de uma transação:


// CreateIndemnites1
    static void CreateIndemnites1() {
      try {
        // session opening 
        using (ISession session = sessionFactory.OpenSession()) {
          // start of transaction
          using (ITransaction transaction = session.BeginTransaction()) {
            // we create two allowances with the same index
            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 };
            // we save them
            session.Save(i1);
            session.Save(i2);
            // commit transaction
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Explicações

  • Nas linhas 9 e 10, são criadas duas entidades Indemnites com o mesmo índice. No entanto, na base de dados, a coluna INDEX tem a restrição UNIQUE.
  • As linhas 12 e 13 colocam as duas entidades Indemnites no contexto de persistência. Este contexto é sincronizado com a base de dados quando a transação é confirmada na linha 15. Esta sincronização irá desencadear duas instruções INSERT. A segunda irá causar uma exceção devido à restrição de unicidade na coluna INDICE. Como estamos dentro de uma transação, a primeira instrução INSERT será revertida.

O resultado é o seguinte:

Effacement base -------------------------------------
--------------- employee deletion
--------------- elimination of allowances
--------------- elimination of contribution rates
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 -------------------------------------
--------------- list of employees
--------------- list of benefits
--------------- table of contribution rates

Linha 9: Podemos ver que a tabela [indemnites] está vazia. Não foram feitas quaisquer inserções.

6.5.6. Criação de dois subsídios com o mesmo índice sem uma transação

O método seguinte cria duas quotas com o mesmo índice sem utilizar uma transação:


// CreateIndemnites2
    static void CreateIndemnites2() {
      try {
        // session opening 
        using (ISession session = sessionFactory.OpenSession()) {
 
          // we create two allowances with the same index
          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 };
          // we save them
          session.Save(i1);
          session.Save(i2);
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Explicações

  • Temos o mesmo código de antes, mas sem uma transação.
  • A sincronização do contexto de persistência com a base de dados ocorrerá quando este contexto for fechado, na linha 13 (ao fechar a sessão). A sincronização irá acionar duas instruções INSERT. A segunda irá falhar devido à restrição de unicidade na coluna INDICE. No entanto, uma vez que não estamos numa transação, a primeira instrução INSERT não será revertida.

O resultado é o seguinte:

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 -------------------------------------
--------------- list of employees
--------------- list of benefits
[1|1,93|2|3|12]
--------------- table of contribution rates

A base de dados estava vazia antes da execução do método. Na linha 6, podemos ver que a tabela [indemnites] tem uma linha.