Skip to content

1. Introducción a ORM NHibernate

El PDF de este documento está disponible |AQUÍ|.

Los ejemplos del documento están disponibles |AQUÍ|.

Este documento es una breve introducción a NHibernate, el equivalente en .NET del framework Java Hibernate. Para una introducción completa, se puede consultar:


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


Un ORM (mapeador objeto-relacional) es un conjunto de bibliotecas que permite a un programa que utiliza una base de datos acceder a ella sin emitir órdenes SQL explícitas y sin conocer las particularidades del SGBD utilizado.


Requisitos previos


En una escala [débutant-intermédiaire-avancé], este documento se encuentra en la sección [intermédiaire]. Para comprenderlo es necesario cumplir varios requisitos previos que se pueden encontrar en algunos de los documentos que he escrito:

  1. Lenguaje C# 2008: [Aprender C# versión 3.0 con .NET Framework 3.5 (2008)]
  1. [Spring IoC], disponible en la URL [Spring IoC para .NET (2005)]. Presenta los fundamentos de la inversión de control (Inversion of Control) o la inyección de dependencias (Dependency Injection) del marco Spring.Net [Spring.NET | Homepage ].

En ocasiones, al principio de los párrafos de este documento se incluyen recomendaciones de lectura que hacen referencia a documentos anteriores.


Herramientas


Las herramientas utilizadas en este caso práctico están disponibles gratuitamente en la web. Son las siguientes (diciembre de 2011):

  • Nhibernate 3.2, disponible en la URL [http://nhforge.org/Default.aspx]
  • Spring.net 1.3.2, disponible en la URL [http://www.springframework.net]. El marco Spring.net es muy completo. Aquí solo utilizaremos la biblioteca que incluye para facilitar el uso del marco Nhibernate.
  • Log4net 1.2.10 disponible en la URL [http://logging.apache.org/log4net]. Este marco de trabajo de registro de logs es utilizado por NHibernate.
  • NUnit 2.5 está disponible en la URL [http://www.nunit.org/]. Este marco de pruebas unitarias es el equivalente para .NET del marco JUnit para la plataforma Java.
  • El controlador ADO.NET 6.4.4 de SGBD MySQL 5 está disponible en la URL [http://dev.mysql.com/downloads/connector/net]

Todos los archivos DLL necesarios para los proyectos de Visual Studio 2010 se han reunido en una carpeta [libnet4]:

 

1.1. El papel de NHIBERNATE en una arquitectura .NET por capas

Una aplicación .NET que utilice una base de datos puede estructurarse en capas de la siguiente manera:

La capa [dao] se comunica con la SGBD a través de la API y la ADO.NET. Recordemos los principales métodos de esta API.

En modo conectado, la aplicación:

  1. abre una conexión con la fuente de datos
  2. trabaja con la fuente de datos en modo lectura/escritura
  3. cierra la conexión

Hay tres interfaces ADO.NET que intervienen principalmente en estas operaciones:

  • IDbConnection, que encapsula las propiedades y los métodos de la conexión.
  • IDbCommand, que encapsula las propiedades y métodos del comando SQL ejecutado.
  • IDataReader, que encapsula las propiedades y métodos del resultado de una orden SQL Select.

La interfaz IDbConnection

Sirve para gestionar la conexión con la base de datos. Entre los métodos M y las propiedades P de esta interfaz se encuentran los siguientes:

Nombre
Tipo
Función
ConnectionString
P
cadena de conexión a la base de datos. Especifica todos los parámetros necesarios para establecer la conexión con una base de datos concreta.
Open
M
abre la conexión con la base de datos definida por ConnectionString
Close
M
cierra la conexión
BeginTransaction
M
inicia una transacción.
State
P
estado de la conexión: ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken

Si Connection es una clase que implementa la interfaz IDbConnection, la conexión se puede establecer de la siguiente manera:

1
2
3
IDbConnection connexion=new Connection();
connexion.ConnectionString=...;
connexion.Open();

La interfaz IDbCommand

Sirve para ejecutar una orden SQL o un procedimiento almacenado. Entre los métodos M y las propiedades P de esta interfaz se encuentran los siguientes:

Nombre
Tipo
Función
CommandType
P
indica lo que hay que ejecutar; toma sus valores de una enumeración:
- CommandType.Text: ejecuta la orden SQL definida en la propiedad CommandText. Es el valor por defecto.
- CommandType.StoredProcedure: ejecuta un procedimiento almacenado en la base de datos
CommandText
P
- el texto de la orden SQL que se ejecutará si CommandType = CommandType.Text
- el nombre del procedimiento almacenado que se ejecutará si CommandType = CommandType.StoredProcedure
Connection
P
la conexión IDbConnection que se utilizará para ejecutar la orden SQL
Transaction
P
la transacción IDbTransaction en la que se ejecutará la orden SQL
Parameters
P
la lista de parámetros de una orden SQL configurada. La orden «update articles set precio=precio*1.1 where id=@id» tiene el parámetro @id.
ExecuteReader
M
para ejecutar una orden SQL Select. Se obtiene un objeto IDataReader que representa el resultado de Select.
ExecuteNonQuery
M
para ejecutar una orden SQL: Actualizar, Insertar, Eliminar. Se obtiene el número de líneas afectadas por la operación (actualizadas, insertadas, eliminadas).
ExecuteScalar
M
para ejecutar una orden SQL. Select solo devuelve un único resultado, como en: select count(*) from articles.
CreateParameter
M
para crear los parámetros IDbParameter de una orden SQL configurada.
Prepare
M
permite optimizar la ejecución de una consulta parametrizada cuando se ejecuta varias veces con parámetros diferentes.

Si Command es una clase que implementa la interfaz IDbCommand, la ejecución de una orden SQL sin transacción tendrá la siguiente forma:

// inicio de sesión 
IDbConnection connexion=...
connexion.Open();
// preparación de la orden
IDbCommand commande=new Command();
commande.Connection=connexion;
// ejecución de la orden «select»
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// ejecución de la orden update, insert, delete
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// cierre de la conexión
connexion.Close();

La interfaz IDataReader

Sirve para encapsular los resultados de una orden SQL Select. Un objeto IDataReader representa una tabla con filas y columnas, que se procesan secuencialmente: primero la primera fila, luego la segunda, etc. Entre los métodos M y las propiedades P de esta interfaz se encuentran los siguientes:

Nombre
Tipo
Función
FieldCount
P
el número de columnas de la tabla IDataReader
GetName
M
GetName(i) devuelve el nombre de la columna n.º i de la tabla IDataReader.
Item
P
Item[i] representa la columna n.º i de la fila actual de la tabla IDataReader.
Read
M
pasa a la siguiente línea de la tabla IDataReader. Devuelve el valor booleano True si se ha podido realizar la lectura; de lo contrario, devuelve False.
Close
M
cierra la tabla IDataReader.
GetBoolean
M
GetBoolean(i): devuelve el valor booleano de la columna n.º i de la línea actual de la tabla IDataReader. Los demás métodos análogos son los siguientes: GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString.
Getvalue
M
Getvalue(i): devuelve el valor de la columna n.º i de la línea actual de la tabla IDataReader como tipo object.
IsDBNull
M
IsDBNull(i) devuelve True si la columna n.º i de la fila actual de la tabla IDataReader no tiene valor, lo que se simboliza con el valor SQL NULL.

La consulta de un objeto IDataReader suele ser similar a lo siguiente:

// apertura de conexión 
IDbConnection connexion=...
connexion.Open();
// preparación de comando
IDbCommand commande=new Command();
commande.Connection=connexion;
// ejecución de la orden SELECT
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// procesamiento de resultados
while(reader.Read()){
     // analizar línea actual
        ...
}
// cierre del lector
reader.Close();
// cierre de la sesión
connexion.Close();

En la arquitectura anterior,

El conector [ADO.NET] está vinculado al SGBD. Por lo tanto, la clase que implementa la interfaz [IDbConnection] es:

  • la clase [MySQLConnection] para SGBD y MySQL
  • la clase [SQLConnection] para SGBD y SQLServer

De este modo, la capa [dao] depende de la capa SGBD utilizada. Algunos marcos de trabajo (Linq, Ibatis.net, NHibernate) eliminan esta restricción añadiendo una capa adicional entre la capa [dao] y el conector [ADO.NET] del SGBD utilizado. En este caso, utilizaremos el marco [NHibernate].

En el ejemplo anterior, la capa [dao] ya no se comunica con el conector [ADO.NET], sino con el marco NHibernate, que le proporcionará una interfaz independiente del conector [ADO.NET] utilizado. Esta arquitectura permite cambiar el SGBD sin modificar la capa [dao]. En ese caso, solo habría que cambiar el conector [ADO.NET].

1.2. La base de datos de ejemplo

Para mostrar cómo trabajar con NHibernate, utilizaremos la siguiente base de datos MySQL [dbpam_nhibernate]:

  • En [1], la base de datos tiene tres tablas:
    • [employes]: una tabla que registra a las empleadas de una guardería
    • [cotisations]: una tabla que registra los tipos de cotización a la Seguridad Social
    • [indemnites]: una tabla que registra la información necesaria para calcular la nómina de las empleadas

Tabla [employes]

  • en [2], la tabla de empleados, y en [3], el significado de sus campos

El contenido de la tabla podría ser el siguiente:

 

Tabla [cotisations]

  • en [4], la tabla de cotizaciones, y en [5], el significado de sus campos

El contenido de la tabla podría ser el siguiente:

 

Tabla [indemnites]

  • en [6], la tabla de indemnizaciones, y en [7], el significado de sus campos

El contenido de la tabla podría ser el siguiente:

 

La exportación de la estructura de la base de datos a un archivo SQL da el siguiente resultado:

#
# Estructura de la tabla «cotizaciones»: 
#

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;

#
# Estructura de la tabla «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;

#
# Estructura de la tabla «employes»: 
#

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;

Cabe destacar, en las líneas 6, 20 y 36, que las claves primarias ID tienen el atributo « » autoincrement. Esto significa que MySQL generará automáticamente los valores de las claves primarias cada vez que se añada un registro. El desarrollador no tiene que preocuparse por ello.

1.3. El proyecto de demostración en C#

Para presentar la configuración y el uso de NHibernate, utilizaremos la siguiente arquitectura:

Un programa de consola [1] manipulará los datos de la base de datos anterior [2] a través del marco [NHibernate] [3]. Esto nos llevará a presentar:

  • los archivos de configuración de NHibernate
  • el API de NHibernate

El proyecto en C# será el siguiente:

Los elementos necesarios para el proyecto son los siguientes:

  • en [1], los archivos DLL que necesita el proyecto:
    • [NHibernate]: el DLL del marco NHibernate
    • [MySql.Data]: el DLL del conector ADO.NET del SGBD MySQL
    • [log4net]: el DLL del framework Log4net que permite generar registros
  • en [2], las clases de imágenes de las tablas de la base de datos
  • en [3], el archivo [App.config] que configura toda la aplicación, incluido el framework [NHibernate]
  • en [4], y las aplicaciones de consola de prueba

1.3.1. Configuración de la conexión a la base de datos

Volvamos a la arquitectura de prueba:

Como se ha indicado anteriormente, [NHibernate] debe poder acceder a la base de datos. Para ello, necesita cierta información:

  • el SGBD que gestiona la base de datos (MySQL, SQLServer, Postgres, Oracle, etc.). La mayoría de los SGBD han añadido al lenguaje SQL extensiones propias. Al conocer el SGBD, el NHibernate puede adaptar los comandos SQL que emite a dicho SGBD. NHibernate utiliza el concepto de dialecto SQL.
  • los parámetros de conexión a la base de datos (nombre de la base, nombre del usuario propietario de la conexión, su contraseña)

Esta información se puede incluir en el archivo de configuración [App.config]. A continuación se muestra el que se utilizará con una base de datos MySQL 5:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <!-- Secciones de configuración -->
    <configSections>
        <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
        <section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
    </configSections>


    <!-- Configuración 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>

    <!-- Esta sección contiene los ajustes de configuración de log4net -->
    <!-- NOTE IMPORTANTE: los registros no están activos por defecto. Hay que activarlos mediante el programa con la instrucción log4net.Config.XmlConfigurator.Configure();
    ! -->
    <log4net>
        <!-- Define un appender de salida (dónde se enviarán los registros) -->
        <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>

        <!-- Configurar la categoría raíz, establecer el nivel de prioridad predeterminado y añadir los appendores (destinación de los registros) -->
        <root>
            <priority value="INFO" />
            <!--
            <appender-ref ref="LogFileAppender" />
            <appender-ref ref="LogDebugAppender"/>
            -->
            <appender-ref ref="ConsoleAppender"/>
        </root>

        <!-- Especificar el nivel para algunos espacios de nombres concretos -->
        <!-- El nivel puede ser: ALL, DEBUG, INFO, WARN, ERROR, FATAL, OFF -->
        <logger name="NHibernate">
            <level value="INFO" />
        </logger>
    </log4net>
</configuration>
  • Las líneas 4-7 definen secciones de configuración en el archivo [App.config]. Veamos la línea 6:

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

Esta línea define la sección de configuración de NHibernate en el archivo [App.config]. Tiene dos atributos: name y type.

  • El atributo [name] da nombre a la sección de configuración. Esta sección debe estar delimitada aquí por las etiquetas <name>...</name>, en este caso <hibernate-configuration>...</hibernate-configuration>, de las líneas 11 a 24.
  • El atributo [type=classe,DLL] indica el nombre de la clase encargada de procesar la sección definida por el atributo [name], así como el DLL que contiene dicha clase. En este caso, la clase se llama [NHibernate.Cfg.ConfigurationSectionHandler] y se encuentra en la clase DLL [NHibernate.dll]. Recordemos que esta clase DLL forma parte de las referencias del proyecto estudiado.

Consideremos ahora la sección de configuración de NHibernate:


    <!-- configuración 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>
  • línea 2: la configuración de NHibernate se encuentra dentro de una etiqueta <hibernate-configuration>. El atributo xmlns (Xml NameSpace) establece la versión utilizada para configurar NHibernate. De hecho, con el paso del tiempo, la forma de configurar NHibernate ha evolucionado. En este caso, se utiliza la versión 2.2.
  • Línea 3: la configuración de NHibernate se encuentra aquí íntegramente dentro de la etiqueta <session-factory> (líneas 3 y 14). Una sesión NHibernate es la herramienta utilizada para trabajar con una base de datos según el esquema:
    • apertura de sesión
    • trabajo con la base de datos mediante los métodos de API NHibernate
    • cierre de sesión

La sesión se crea mediante un factory, un término genérico que designa una clase capaz de crear objetos. Las líneas 3-14 configuran este factory.

  • líneas 4, 6, 8 y 9: configuran la conexión a la base de datos de destino. La información principal es el nombre del SGBD utilizado, el nombre de la base de datos, la identidad del usuario y su contraseña.
  • línea 4: define el proveedor de la conexión, es decir, aquel al que se solicita una conexión con la base de datos. El valor de la propiedad [connection.provider] es el nombre de una clase NHibernate. Esta propiedad no depende del SGBD utilizado.
  • línea 6: el controlador ADO.NET que se va a utilizar. Es el nombre de una clase NHibernate especializada para un SGBD determinado, en este caso MySQL. La línea 6 se ha comentado, ya que no es imprescindible.
  • Línea 8: la propiedad [dialect] establece el dialecto SQL que se va a utilizar con el SGBD. En este caso, se trata del dialecto de SGBD MySQL.

Si cambiamos a SGBD, ¿cómo se encuentra el dialecto NHibernate correspondiente? Volvamos al proyecto de C# anterior y hagamos doble clic en el DLL [NHibernate] en la pestaña [References]:

  • en [1], la pestaña [Explorateur d'objets] muestra una serie de DLL, entre las que se encuentran las referenciadas por el proyecto.
  • En [2], la DLL y la [NHibernate]
  • en [3], la DLL y la [NHibernate] desarrolladas. En ellas se encuentran los distintos espacios de nombres (namespace) que se definen en ellas.
  • En [4], el espacio de nombres [NHibernate.Dialect], donde se encuentran las clases que definen los distintos dialectos SQL que se pueden utilizar.
  • En [5], la clase del dialecto de SGBD MySQL 5.
  • en [6], el espacio de nombres de la clase [MySqlDataDriver] utilizada en la línea 6 a continuación:

    <!-- configuración 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>
  • líneas 9-11: la cadena de conexión a la base de datos. Esta cadena tiene el formato «param1=val1;param2=val2; ...». El conjunto de parámetros así definidos permite al controlador de SGBD establecer una conexión. El formato de esta cadena de conexión depende del controlador SGBD utilizado. Las cadenas de conexión de los principales controladores SGBD se pueden encontrar en la página web [http://www.connectionstrings.com/]. En este caso, la cadena «Server=localhost;Database=dbpam_nhibernate;Uid=root;Pwd=;» es una cadena de conexión para el SGBD MySQL. Indica que:
    • Server=localhost;: el servidor SGBD se encuentra en el mismo equipo que el cliente que intenta establecer la conexión
    • Database=dbpam_nhibernate; : la base de datos MySQL de destino
    • Uid=root;: el usuario que abre la conexión es el usuario root
    • Pwd=;: este usuario no tiene contraseña (caso particular de este ejemplo)
  • línea 12: la propiedad [show_sql] indica si NHibernate debe mostrar en sus registros las órdenes SQL que envía a la base de datos. Durante la fase de desarrollo, resulta útil establecer esta propiedad en [true] para saber exactamente qué hace NHibernate.
  • Línea 13: para entender la etiqueta <mapping>, volvamos a la arquitectura de la aplicación:

Si el programa de consola fuera un cliente directo del conector ADO.NET y quisiera obtener la lista de empleados, haría que el conector ejecutara una orden SQL Select, y recibiría a cambio un objeto de tipo IDataReader que tendría que procesar para obtener la lista de empleados deseada inicialmente.

En el ejemplo anterior, el programa de consola es el cliente de NHibernate y NHibernate es el cliente del conector ADO.NET. Más adelante veremos que el API de NHibernate permitirá al programa de consola solicitar la lista de empleados. NHibernate traducirá esta solicitud en una orden SQL Select que hará que ejecute el conector ADO.NET. Este le devolverá un objeto de tipo IDataReader. A partir de este objeto, NHibernate debe ser capaz de construir la lista de empleados que se le ha solicitado. Esto es posible gracias a la configuración. A cada tabla de la base de datos le corresponde una clase C#. Así, a partir de las filas de la tabla [employes] devueltas por IDataReader, NHibernate podrá construir una lista de objetos que representan a los empleados y devolverla al programa de consola. Estas relaciones entre tablas y clases se crean en archivos de configuración. NHibernate utiliza el término «mapping» para definir estas relaciones.

Volvamos a la línea 13 que se muestra a continuación:


    <!-- configuración 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 línea 13 indica que los archivos de configuración de tablas <--> clases se encontrarán en el ensamblado [pam-nhibernate-demos]. Un ensamblado es el ejecutable o el DLL resultante de la compilación de un proyecto. En este caso, los archivos de mapeo se colocarán en el ensamblado del proyecto de ejemplo. Para conocer el nombre de este ensamblado, hay que consultar las propiedades del proyecto:

  • en [1], las propiedades del proyecto
  • en la pestaña [Application] [2], el nombre del ensamblado [3] que se va a generar.
  • Dado que el tipo de salida es [Application console] [4], el archivo generado al compilar el proyecto se llamará [pam-nhibernate-demos.exe]. Si el tipo de salida fuera [Bibliothèque de classes] [5], el archivo generado al compilar el proyecto se llamaría [pam-nhibernate-demos.dll]
  • El ensamblado se genera en la carpeta [bin/Release] del proyecto [6].

De la explicación anterior hay que recordar que los archivos de las tablas de correspondencia <--> clases deberán encontrarse en el archivo [pam-nhibernate-demos.exe] [6].

1.3.2. Configuración del e de las tablas de mapeo <--> clases

Volvamos a la arquitectura del proyecto analizado:

  • en [1], el programa de consola utiliza los métodos de API del marco NHibernate. Estos dos bloques intercambian objetos.
  • En [2], NHibernate utiliza el API de un conector .NET. Envía órdenes SQL al SGBD de destino.

El programa de consola manipulará objetos que reflejan las tablas de la base de datos. En este proyecto, estos objetos y los enlaces que los unen a las tablas de la base de datos se han colocado en la carpeta [Entites] que aparece a continuación:

 
  • cada tabla de la base de datos cuenta con una clase y un archivo de mapeo entre ambas
Tabla
Clase
Asignación
cotisations
Cotisations.cs
Cotisations.hbm.xml
employes
Employe.cs
Employe.hbm.xml
indemnites
Indemnites.cs
Indemnites.hbm.xml

1.3.2.1. Asignación de la tabla [cotisations]

Consideremos la tabla [cotisations]:

ID
clave primaria de tipo autoincremento
VERSION
N.º de versión del registro
SECU
tipo (porcentaje) de cotización a la Seguridad Social
RETRAITE
Tasa de cotización para la jubilación
CSGD
tipo de cotización de la contribución social generalizada deducible
CSGRDS
Tasa de cotización para la contribución social generalizada y la contribución al reembolso de la deuda social

Una línea de esta tabla puede encapsularse en un objeto de tipo [Cotisations.cs] de la siguiente manera:


namespace PamNHibernateDemos {
    public class Cotisations {
        // propiedades automáticas
        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; }

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

}

Se ha creado una propiedad automática para cada una de las columnas de la tabla [cotisations]. Cada una de estas propiedades debe declararse virtual (virtual), ya que NHibernate va a derivar de la clase y a redefinir (override) sus propiedades. Por lo tanto, estas deben ser virtuales.

Cabe señalar, en la línea 1, que la clase pertenece al espacio de nombres [PamNHibernateDemos].

El archivo de mapeo [Cotisations.hbm.xml] entre la tabla [cotisations] y la clase [Cotisations] es el siguiente:


<?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>
  • El archivo de mapeo es un archivo XML definido dentro de la etiqueta <hibernate-mapping> (líneas 2 y 14)
  • línea 4: la etiqueta <class> establece el vínculo entre una tabla de la base de datos y una clase. En este caso, la tabla [COTISATIONS] (atributo table) y la clase [Cotisations] (atributo name). En .NET, una clase debe definirse mediante su nombre completo (espacio de nombres incluido) y mediante el ensamblado que la contiene. Estos dos datos se indican en la línea 3. El primero (espacio de nombres) puede encontrarse en la definición de la clase. La segunda (ensamblado) es el nombre del ensamblado del proyecto. Ya hemos indicado cómo encontrar este nombre.
  • líneas 5-7: la etiqueta <id> sirve para definir la asignación de la clave primaria de la tabla [cotisations].
    • línea 5: el atributo name designa el campo de la clase [Cotisations] que va a recibir la clave primaria de la tabla [cotisations]. El atributo column designa la columna de la tabla [cotisations] que sirve como clave primaria. El atributo unsaved-value sirve para definir una clave primaria aún no generada. Este valor permite a NHibernate saber cómo guardar un objeto [Cotisations] en la tabla [cotisations]. Si este objeto tiene un campo Id=0, realizará una operación SQL INSERT; en caso contrario, realizará una operación SQL UPDATE. El valor de unsaved-value depende del tipo del campo Id de la clase [Cotisations]. En este caso, es de tipo int y el valor por defecto de un tipo int es 0. Por lo tanto, un objeto [Cotisations] que aún no se haya guardado (y que, por lo tanto, no tenga clave primaria) tendrá su campo Id=0. Si el campo Id hubiera sido de tipo Object o un derivado, se habría escrito unsaved-value=null.
    • línea 6: cuando NHibernate debe guardar un objeto [Cotisations] con un campo Id=0, debe realizar en la base de datos una operación INSERT durante la cual debe obtener un valor para la clave primaria del registro. La mayoría de los SGBD cuentan con un método propio para generar automáticamente este valor. La etiqueta <generator> sirve para definir el mecanismo que se debe utilizar para la generación de la clave primaria. La etiqueta <generator class="native"> indica que se debe utilizar el mecanismo por defecto del SGBD utilizado. En el apartado 1.2 hemos visto que las claves primarias de nuestras tres tablas MySQL tenían el atributo autoincrement. Durante sus operaciones, INSERT y NHibernate no proporcionarán ningún valor a la columna ID del registro añadido, dejando que MySQL genere dicho valor.
  • línea 8: la etiqueta <version> sirve para definir la columna de la tabla (así como el campo de la clase correspondiente) que permite «versionar» los registros. Inicialmente, la versión es 1. Se incrementa con cada operación UPDATE. Por otra parte, cualquier operación UPDATE o DELETE se realiza con un filtro WHERE ID= id AND VERSION=v1. Por lo tanto, un usuario solo puede modificar o eliminar un objeto si dispone de la versión correcta del mismo. Si no es así, NHibernate genera una excepción.
  • línea 9: la etiqueta <property> sirve para definir una asignación de columna normal (ni clave primaria ni columna de versión). Así, la línea 9 indica que la columna CSGRDS de la tabla [COTISATIONS] está asociada a la propiedad CsgRds de la clase [Cotisations].

1.3.2.2. Asignación de la tabla [indemnites]

Consideremos la tabla [indemnites]:

ID
clave primaria de tipo autoincremento
VERSION
N.º de versión del registro
BASE_HEURE
coste en euros de una hora de guardia
ENTRETIEN_JOUR
Indemnización en euros por día de guardia
REPAS_JOUR
Indemnización por comida en euros por día de guardia
INDEMNITES_CP
Indemnizaciones por vacaciones pagadas. Se trata de un porcentaje que se aplica al salario base.

Una línea de esta tabla puede encapsularse en un objeto de tipo [Indemnites] de la siguiente manera:


namespace PamNHibernateDemos {
    public class Indemnites {

        // propiedades automáticas
        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; }

        // constructores
        public Indemnites() {
        }

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

    }
}

El archivo de asignación de la tabla [indemnites] <--> clase [Indemnites] podría ser el siguiente (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>

No hay nada nuevo aquí con respecto al archivo de mapeo explicado anteriormente. La única diferencia se encuentra en la línea 9. El atributo unique="true" indica que en la tabla [indemnites] existe una restricción de unicidad en la columna [INDICE]: no puede haber dos filas con el mismo valor en la columna [INDICE].

1.3.2.3. Asignación de la tabla [employes]

Consideremos la tabla [employes]:

ID
clave primaria de tipo autoincremento
VERSION
N.º de versión del registro
PRENOM
nombre del empleado
NOM
su apellido
ADRESSE
su dirección
CP
su código postal
VILLE
su ciudad
INDEMNITE_ID
clave externa en INDEMNITES (ID)

La novedad con respecto a las tablas anteriores es la presencia de una clave externa: la columna [INDEMNITE_ID] es una clave externa sobre la columna [ID] de la tabla [INDEMNITES]. Este campo hace referencia a la fila de la tabla [INDEMNITES] que se debe utilizar para calcular las prestaciones del empleado.

La clase [Employe] a imagen de la tabla [employes] podría ser la siguiente:


namespace PamNHibernateDemos {
    public class Employe {
        // propiedades automáticas
        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; }

        // constructores
        public Employe() {
        }

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

El archivo de asignación [Employe.hbm.xml] podría ser el siguiente:


<?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 novedad se encuentra en la línea 15, con la aparición de una nueva etiqueta: <many-to-one>. Esta etiqueta sirve para asignar una columna de clave externa [INDEMNITE_ID] de la tabla [EMPLOYES] a la propiedad [Indemnites] de la clase [Employe]:


namespace PamNHibernateDemos {
    public class Employe {
        // propiedades automáticas
..
        public virtual Indemnites Indemnites { get; set; }

...
    }
}

La tabla [EMPLOYES] tiene una clave externa [INDEMNITE_ID] que hace referencia a la columna [ID] de la tabla [INDEMNITES]. Varias (many) filas de la tabla [EMPLOYES] pueden hacer referencia a una misma fila (one) de la tabla [INDEMNITES]. De ahí el nombre de la etiqueta <many-to-one>. Esta etiqueta tiene aquí los siguientes atributos:

  • column: indica el nombre de la columna de la tabla [EMPLOYES] que actúa como clave externa en la tabla [INDEMNITES]
  • name: indica la propiedad de la clase [Employe] asociada a esta columna. El tipo de esta propiedad es necesariamente la clase asociada a la tabla de destino de la clave foránea, en este caso la tabla [INDEMNITES]. Sabemos que esta clase es la clase [Indemnites] ya descrita. Esto es lo que refleja la línea 5 anterior. Significa que cuando NHibernate recupere de la base de datos un objeto [Employe], también recuperará el objeto [Indemnites] que le corresponde.
  • cascada: este atributo puede tener varios valores:
    • save-update: una operación de inserción (save) o de actualización (update) en el objeto [Employe] debe propagarse al objeto [Indemnites] que contiene.
    • delete: la eliminación de un objeto [Employe] debe propagarse al objeto [Indemnites] que contiene.
    • all: propaga las operaciones de inserción (save), actualización (update) y eliminación (delete).
    • none: no propaga nada

Para terminar, recordemos la configuración de NHibernate en el archivo [App.config]:


    <!-- configuración 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 línea 13 indica que los archivos de mapeo *.hbm.xml se encontrarán en el ensamblado [pam-nhibernate-demos]. Esto no ocurre de forma predeterminada. Hay que configurarlo en el proyecto C#:

  • en [1], se seleccionan las propiedades de un archivo de mapeo
  • en [2], la acción de generación debe ser [Ressource incorporée] [3]. Esto significa que, al generar el proyecto, el archivo de mapeo debe incorporarse al ensamblado generado.

1.4. API a partir de NHibernate

Volvamos a la arquitectura de nuestro proyecto de ejemplo:

En los párrafos anteriores, hemos configurado NHibernate de dos maneras:

  • en [App.config], hemos configurado la conexión a la base de datos
  • hemos definido, para cada tabla de la base de datos, la clase que representa dicha tabla y el archivo de mapeo que permite pasar de la clase a la tabla y viceversa.

Ahora nos queda descubrir los métodos que ofrece NHibernate para manipular los datos de la base de datos: inserción, actualización, eliminación y listado.

1.4.1. El objeto SessionFactory

Toda operación NHibernate se realiza dentro de una sesión. Una secuencia típica de operaciones NHibernate es la siguiente:

  • abrir una sesión NHibernate
  • iniciar una transacción en la sesión
  • realizar operaciones de persistencia con la sesión (Load, Get, Find, CreateQuery, Save, SaveOrUpdate, Delete)
  • validar (commit) o anular (rollback) la transacción
  • cerrar la sesión NHibernate

La sesión se obtiene a través de una fábrica de tipo [SessionFactory]. Esta fábrica es la configurada mediante la etiqueta <session-factory> en el archivo de configuración [App.config]:


    <!-- configuración 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>

En código C#, la SessionFactory se puede obtener de la siguiente manera:


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

La clase Configuration es una clase del marco NHibernate. La instrucción anterior utiliza la sección de configuración de NHibernate en [App.config]. El objeto [ISessionFactory] obtenido contiene entonces la:

  • información para establecer una conexión con la base de datos de destino
  • archivos de mapeo entre las tablas de la base de datos y las clases persistentes gestionadas por NHibernate.

1.4.2. La sesión NHibernate

Una vez creada la sesión SessionFactory (esto se hace una sola vez), se pueden obtener las sesiones que permiten realizar operaciones de persistencia NHibernate. Un código habitual es el siguiente:


try{
      // inicio de sesión 
      using (ISession session = sessionFactory.OpenSession())
      {
        // inicio de la transacción
        using (ITransaction transaction = session.BeginTransaction())
        {
........................ opérations de persistance
          // validación de la transacción
          transaction.Commit();
        }
      }
}catch (Exception ex){
....
}
  • línea 3: se crea una sesión a partir de SessionFactory dentro de una cláusula «using». Al salir de la cláusula using, la sesión se cerrará automáticamente. Sin la cláusula using, habría que cerrar la sesión de forma explícita (session.Close()).
  • Línea 6: las operaciones de persistencia se realizarán dentro de una transacción. O bien se completan todas con éxito, o bien ninguna se completa con éxito. Dentro de la cláusula using,, la transacción se valida mediante un Commit (línea 10). Si, dentro de la transacción, una operación de persistencia lanza una excepción, la transacción quedará automáticamente invalidada mediante un Rollback al salir del using.
  • El bloque «try / catch» de las líneas 1 y 13 permite interceptar cualquier excepción generada por el código dentro del bloque «try» (sesión, transacción, persistencia).

1.4.3. La interfaz ISession

A continuación, presentamos algunos de los métodos de la interfaz ISession implementada por una sesión NHibernate:

ITransaction BeginTransaction()
inicia una transacción en la sesión
ITransaction tx=session.BeginTransaction();
void Clear()
vacía la sesión. Los objetos que contenía quedan desvinculados.
session.Clear();
void Close()
cierra la sesión. Los objetos que contenía se sincronizan con la base de datos. Esta operación de sincronización también se realiza al final de una transacción. Este último caso es el más habitual.
session.Close();
IQuery CreateQuery(string queryString)
crea una consulta HQL (Hibernate Query Language) para su ejecución posterior.
IQuery query=session.createQuery("select e from Empleado e);
void Delete(object obj)
Elimina un objeto. Este puede pertenecer a la sesión (vinculado) o no (desvinculado). Al sincronizar la sesión con la base de datos, se realizará una operación SQL DELETE sobre este objeto.
// se carga un empleado de la BD
Empleado e = session.Get<Empleado>(143);
// se elimina
session.Delete(e);
void Flush()
fuerza la sincronización de la sesión con la base de datos. El contenido de la sesión no cambia.
session.Flush();
T Get<T>(object id)
busca en la base de datos el objeto T con la clave primaria id. Si este objeto no existe, establece el puntero en nulo.
// se carga un empleado de la BD
Empleado e = session.Get<Empleado>(143);
object Save(object obj)
introduce el objeto obj en la sesión. Este objeto no tiene clave primaria antes de la operación Save. Después de Save, sí tiene una. Durante la sincronización de la sesión, se realizará una operación SQL INSERT en la base de datos.
// creamos un empleado
Empleado e = new Empleado(){...};
// lo guardamos
e = session.Save(e);
SaveOrUpdate(object obj)
Realiza una operación Save si el objeto no tiene clave primaria, o una operación Update si ya tiene una.
void Update(object obj)
actualiza el objeto obj en la base de datos. A continuación, se realiza una operación SQL UPDATE en la base de datos.
// se carga un empleado de la operación BD
Empleado e = session.Get<Empleado>(143);
// se cambia su nombre
e.Nom = ...;
// se actualiza en la base de datos
session.Update(e);

1.4.4. La interfaz IQuery

La interfaz IQuery permite realizar consultas a la base de datos para extraer datos. Ya hemos visto cómo crear una instancia:

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

El parámetro del método createQuery es una consulta HQL (Hibernate Query Language), un lenguaje similar al SQL, pero que consulta clases en lugar de tablas. La consulta anterior solicita la lista de todos los empleados. A continuación se muestran algunos ejemplos 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

A continuación, presentamos algunos de los métodos de la interfaz IQuery:

IList<T> List<T>()
devuelve el resultado de la consulta en forma de una lista de objetos T
IList<Empleado> empleados=session.createQuery("select e from Empleado e order by e.Nom asc").List<Empleado>();
IList List()
devuelve el resultado de la consulta en forma de lista, en la que cada elemento de la lista representa una fila del resultado de la consulta Select en forma de matriz de objetos.
IList líneas = session.createQuery("select e.Nom, e.Prenom, e.SS from Empleado").List();
líneas[i][j] representa la columna j de la línea i en un tipo «object». Así, líneas[10][1] es un tipo «object» que representa el nombre de pila de una persona. Por lo general, es necesario realizar conversiones de tipos para recuperar los datos en su tipo exacto.
T UniqueResult<T>()
devuelve el primer objeto del resultado de la consulta
Empleado e=session.createQuery("select e from Empleado e where e.Nom='MARTIN'").UniqueResult<Empleado>();

Se puede configurar una consulta HQL:

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

En la consulta HQL de la línea 3, :num es un parámetro que debe recibir un valor antes de que se ejecute la consulta. En el ejemplo anterior, se utiliza el método SetString para ello. La interfaz IQuery dispone de varios métodos Set para asignar un valor a un parámetro:

  • - SetBoolean(string name, bool value)
  • - SetSingle(string name, single value)
  • - SetDouble(cadena nombre, doble valor)
  • - SetInt32(nombre: cadena, valor: int32)
  • ..

1.5. Algunos ejemplos de código

Los ejemplos que siguen se basan en la arquitectura estudiada anteriormente y que se resume a continuación. La base de datos es la base de datos MySQL [dbpam_nhibernate], también presentada. Los ejemplos son programas de consola [1] que utilizan el marco de trabajo NHibernate [3] para manipular la base de datos [2].

El proyecto en C# en el que se incluyen los ejemplos que siguen es el que ya se ha presentado:

  • en [1], los DLL que necesita el proyecto:
    • [NHibernate]: el archivo DLL del marco de trabajo NHibernate
    • [MySql.Data]: el DLL del conector ADO.NET del SGBD MySQL 5
    • [log4net]: la DLL de una herramienta que permite generar registros
  • en [2], las clases de imágenes de las tablas de la base de datos
  • en [3], el archivo [App.config] que configura toda la aplicación, incluido el marco de trabajo [NHibernate]
  • en [4], y las aplicaciones de consola de prueba. Son estas últimas las que vamos a presentar parcialmente.

1.5.1. Obtener el contenido de la base de datos

El programa [ShowDataBase.cs] permite visualizar el contenido de la base de datos:


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


namespace PamNHibernateDemos
{
  public class ShowDataBase
  {

    private static ISessionFactory sessionFactory = null;

    // programa principal
    static void Main(string[] args)
    {
      // inicialización de la fábrica NHibernate
      sessionFactory = new Configuration().Configure().BuildSessionFactory();
      try
      {
        // visualización del contenido de la base de datos
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase1();
      }
      catch (Exception ex)
      {
        // se muestra la excepción 
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
      }
      finally
      {
        if (sessionFactory != null)
        {
          sessionFactory.Close();
        }
      }
      // espera de entrada del teclado
      Console.ReadLine();
    }

    // prueba1
    static void ShowDataBase1()
    {
      // inicio de sesión 
      using (ISession session = sessionFactory.OpenSession())
      {
        // Inicio de transacción
        using (ITransaction transaction = session.BeginTransaction())
        {
          // se recupera la lista de empleados
          IList<Employe> employes = session.CreateQuery(@"select e from Employe e order by e.Nom asc").List<Employe>();
          // se muestra
          Console.WriteLine("--------------- liste des employés");
          foreach (Employe e in employes)
          {
            Console.WriteLine(e);
          }
          // se recupera la lista de indemnizaciones
          IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i order by i.Indice asc").List<Indemnites>();
          // se muestra
          Console.WriteLine("--------------- liste des indemnités");
          foreach (Indemnites i in indemnites)
          {
            Console.WriteLine(i);
          }
          // se recupera la lista de cotizaciones
          Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
          Console.WriteLine("--------------- tableau des taux de cotisations");
          Console.WriteLine(cotisations);
          // confirmar transacción
          transaction.Commit();
        }
      }
    }
  }
}

Explicaciones:

  • línea 19: se crea el objeto SessionFactory. Este es el que nos permitirá obtener los objetos Session.
  • línea 24: se muestra el contenido de la base de datos
  • líneas 31-37: se cierra el objeto SessionFactory en la cláusula finally del objeto try.
  • línea 43: el método que muestra el contenido de la base
  • línea 46: se obtiene una Session a partir de la SessionFactory.
  • línea 49: se inicia una transacción
  • línea 52: consulta HQL para recuperar la lista de empleados. Debido a la clave externa que vincula la entidad Employe con la entidad Indemnite, junto con cada empleado aparecerá su indemnización.
  • línea 60: consulta HQL para obtener la lista de indemnizaciones.
  • línea 68: consulta HQL para obtener la única línea de la tabla de cotizaciones.
  • línea 72: fin de la transacción
  • línea 73: fin del «using Itransaction» de la línea 49; la transacción se cierra automáticamente
  • línea 74: fin del «using Isession» de la línea 46; la sesión se cierra automáticamente.

Pantalla obtenida:

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]

Cabe destacar, en las líneas 3 y 4, que al consultar los datos de un empleado, también se ha obtenido su indemnización.

1.5.2. Insertar datos en la base de datos

El programa [FillDataBase.cs] permite introducir datos en la base de datos:


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


namespace PamNHibernateDemos
{
  public class FillDataBase
  {

    private static ISessionFactory sessionFactory = null;

    // programa principal
    static void Main(string[] args)
    {
      // inicialización de la fábrica NHibernate
      sessionFactory = new Configuration().Configure().BuildSessionFactory();
      try
      {
        // eliminación del contenido de la base de datos
        Console.WriteLine("Effacement base -------------------------------------");
        ClearDataBase1();
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase();
        Console.WriteLine("Remplissage base -------------------------------------");
        FillDataBase1();
        Console.WriteLine("Affichage base -------------------------------------");
        ShowDataBase();
      }
      catch (Exception ex)
      {
        // se muestra la excepción 
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : [{0}]", ex.ToString()));
      }
      finally
      {
        if (sessionFactory != null)
        {
          sessionFactory.Close();
        }
      }
      // espera de entrada del teclado
      Console.ReadLine();
    }

    // prueba1
    static void ShowDataBase()
    {
             // véase el ejemplo anterior
    }

    // ClearDataBase1
    static void ClearDataBase1()
    {
      // inicio de sesión 
      using (ISession session = sessionFactory.OpenSession())
      {
        // inicio de transacción
        using (ITransaction transaction = session.BeginTransaction())
        {
          // se recupera la lista de empleados
          IList<Employe> employes = session.CreateQuery(@"select e from Employe e").List<Employe>();
          // se eliminan todos los empleados
          Console.WriteLine("--------------- suppression des employés associés");
          foreach (Employe e in employes)
          {
            session.Delete(e);
          }
          // se recupera la lista de indemnizaciones
          IList<Indemnites> indemnites = session.CreateQuery(@"select i from Indemnites i").List<Indemnites>();
          // se eliminan las indemnizaciones
          Console.WriteLine("--------------- suppression des indemnités");
          foreach (Indemnites i in indemnites)
          {
            session.Delete(i);
          }
          // se recupera la lista de cotizaciones
          Cotisations cotisations = session.CreateQuery(@"select c from Cotisations c").UniqueResult<Cotisations>();
          Console.WriteLine("--------------- suppression des taux de cotisations");
          if (cotisations != null)
          {
            session.Delete(cotisations);
          }
          // confirmar transacción
          transaction.Commit();
        }
      }
    }

    // FillDataBase
    static void FillDataBase1()
    {
      // inicio de sesión 
      using (ISession session = sessionFactory.OpenSession())
      {
        // Inicio de la transacción
        using (ITransaction transaction = session.BeginTransaction())
        {
          // se crean dos indemnizaciones
          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 };
          // se crean dos empleados
          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 };
          // se crean las tasas de cotización
          Cotisations cotisations = new Cotisations() { Id = 0, CsgRds = 3.49, Csgd = 6.15, Secu = 9.39, Retraite = 7.88 };
          // se guarda todo
          session.Save(e1);
          session.Save(e2);
          session.Save(cotisations);
          // confirmar transacción
          transaction.Commit();
        }
      }
    }

  }
}

Explicaciones

  • línea 19: se crea el SessionFactory
  • líneas 37-43: se cierra en la cláusula finally del try
  • línea 55: el método ClearDataBase1, que vacía la base de datos. El principio es el siguiente:
    • se recogen todos los empleados (línea 64) en una lista
    • se eliminan uno a uno (líneas 67-70)
  • línea 93: el método FillDataBase1 inserta algunos datos en la base de datos
  • se crean dos entidades Indemnites (líneas 102 y 103)
  • se crean dos empleados con estas indemnizaciones (líneas 105 y 106)
  • se crea un objeto Cotisations en la línea 108.
  • líneas 110 y 111: las dos entidades «Empleado» se guardan en la base de datos
  • línea 112: a su vez, se guarda la entidad «Cotizaciones»
  • Puede resultar sorprendente que las entidades Indemnités de las líneas 102 y 103 no se hayan guardado. De hecho, se guardaron al mismo tiempo que las entidades Employe. Para entenderlo, hay que volver a la asignación de la entidad 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 línea 15, que asigna la relación de clave externa de la entidad Employe a la entidad Indemnites, tiene el atributo cascade= «save-update», lo que implica que las operaciones «save » y «update» de la entidad Employe se propaguen a la entidad interna Indemnites.

Pantalla resultante:

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]

1.5.3. Búsqueda de un empleado

El programa [Program.cs] cuenta con diversos métodos que ilustran el acceso y la manipulación de los datos de la base de datos. A continuación presentamos algunos de ellos.

El método [FindEmployee] permite buscar a un empleado por su número de la Seguridad Social:


// FindEmployee
    static void FindEmployee() {
      try {
        // inicio de sesión 
        using (ISession session = sessionFactory.OpenSession()) {
          // inicio de la transacción
          using (ITransaction transaction = session.BeginTransaction()) {
            // búsqueda de un empleado por su número 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é...");
            }

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

Explicaciones

  • línea 10: la consulta Select configurada por numSecu que se va a ejecutar
  • línea 11: la asignación de un valor al parámetro numSecu y la ejecución del método UniqueResult para obtener un único resultado.

Pantalla resultante:

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é...

1.5.4. Inserción de entidades no válidas

El siguiente método intenta guardar una entidad [Employe] no inicializada.


// SaveEmptyEmployee
    static void SaveEmptyEmployee() {
      try {
        // inicio de sesión 
        using (ISession session = sessionFactory.OpenSession()) {
          // inicio de transacción
          using (ITransaction transaction = session.BeginTransaction()) {
            // se crea un empleado sin datos
            Employe e = new Employe();
            // se crea una indemnización inexistente
            Indemnites i = new Indemnites() { Id = 0, Indice = 3, BaseHeure = 1.93, EntretienJour = 2, RepasJour = 3, IndemnitesCp = 12 };
            // que se asocia al empleado
            e.Indemnites = i;
            // se guarda el empleado dejando vacíos los demás campos
            session.Save(e);
            // se confirma la transacción
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Explicaciones

Recordemos el código de la clase [Employe]:


namespace PamNHibernateDemos {
    public class Employe {
        // propiedades automáticas
        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; }

        // constructores
        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 objeto [Employe] no inicializado tendrá el valor «null» en todos sus campos de tipo cadena. Al insertar el registro en la tabla [employes], NHibernate dejará vacías las columnas correspondientes a dichos campos. Sin embargo, en la tabla [employes], todas las columnas tienen el atributo «not null», lo que impide que haya columnas sin valor. Por lo tanto, el controlador ADO.NET lanzará una excepción:

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 (?, ?, ?, ?, ?, ?, ?, ?)]

1.5.5. Creación de dos indemnizaciones con el mismo índice dentro de una transacción

En la tabla [indemnites], la columna [indice] se ha declarado con el atributo «unique», lo que impide que haya dos filas con el mismo índice. El siguiente método crea dos indemnizaciones con el mismo índice dentro de una transacción:


// CreateIndemnites1
    static void CreateIndemnites1() {
      try {
        // inicio de sesión 
        using (ISession session = sessionFactory.OpenSession()) {
          // inicio de la transacción
          using (ITransaction transaction = session.BeginTransaction()) {
            // se crean dos indemnizaciones con el mismo índice
            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 };
            // se guardan
            session.Save(i1);
            session.Save(i2);
            // confirmación de la transacción
            transaction.Commit();
          }
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Explicaciones

  • En las líneas 9 y 10 se crean dos entidades Indemnites con el mismo índice. Sin embargo, en la base de datos, la columna INDICE tiene el atributo UNIQUE.
  • Las líneas 12 y 13 colocan las dos entidades Indemnites en el contexto de persistencia. Este se sincroniza con la base de datos al validar la transacción de la línea 15. Esta sincronización dará lugar a dos INSERT. El segundo provocará una excepción debido a la unicidad de la columna INDICE. Como nos encontramos dentro de una transacción, el primer INSERT se revertirá.

El resultado obtenido es el siguiente:

Effacement base -------------------------------------
--------------- eliminación de empleados
--------------- eliminación de indemnizaciones
--------------- eliminación de las tasas de cotización
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 -------------------------------------
--------------- lista de empleados
--------------- lista de indemnizaciones
--------------- tabla de tipos de cotización

En la línea 9, se puede ver que la tabla [indemnites] está vacía. No se ha producido ninguna inserción.

1.5.6. Creación de dos indemnizaciones del mismo índice fuera de una transacción

El siguiente método crea dos indemnizaciones con el mismo índice sin utilizar ninguna transacción:


// CreateIndemnites2
    static void CreateIndemnites2() {
      try {
        // inicio de sesión 
        using (ISession session = sessionFactory.OpenSession()) {

          // se crean dos prestaciones con el mismo índice
          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 };
          // se guardan
          session.Save(i1);
          session.Save(i2);
        }
      } catch (Exception e) {
        Console.WriteLine("L'exception suivante s'est produite : " + e.Message);
      }
    }

Explicaciones

  • Tenemos el mismo código que antes, pero sin transacción.
  • La sincronización del contexto de persistencia con la base de datos se realizará al cerrar dicho contexto, en la línea 13 (cierre de Session). La sincronización provocará dos INSERT. El segundo fallará debido a la unicidad de la columna INDICE. Pero, como no estamos en una transacción, el primer INSERT no se revertirá.

El resultado obtenido es el siguiente:

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 -------------------------------------
--------------- lista de empleados
--------------- lista de indemnizaciones
[1|1,93|2|3|12]
--------------- tabla de tipos de cotización

La base de datos estaba vacía antes de ejecutar el método. En la línea 6, se puede ver que la tabla [indemnites] tiene una fila.