darioquintana.com.ar

Tutorial de NHibernate - Primeros pasos

Ultima revisión: 4 de junio 2007

NHibernate es un framework de O/RM (Object/Relational Mapping), un port de Hibernate de Java, que tiene como función principal mapear los objetos desde una aplicación .Net a una base de datos Relacional. Primero, hablemos de como NH (NHibernate) realiza su funcionamiento básico.

Código de ejemplo

En el siguiente ejemplo será solamente puntapié inicial para introducirnos en este framework de persistencia, si bien contiene algunos consejos como buenas prácticas pero no es lo que persigue, por esta razón los ejemplos serán sencillos. De hecho, el modelo posee una clara correspondencia de 1 objeto => 1 tabla, lo cual en la realidad no debe buscarse, ya que los modelos de dominios y de datos buscan resolver problemas diferentes, y si estamos en presencia de esto, es muy probable que no estemos explotando todo el potencial de ambos modelos, y por sobre todo de esta herramienta. NHibernate nos da la libertad de realizar un modelo de dominio, y después mapearlo a un modelo de tablas, y viceversa. El código de ejemplo lo pueden descargar aquí Dario.NH01.zip

Modelo de Dominio

El modelo de dominio con el cual trabajaremos será el siguiente:

Modelo de Dominio

Modelo relacional

El modelo de datos estará representado por estas tres relaciones (tablas):

Modelo de tablas

Mappings Files ó Archivos de Mapeo:

NH para poder conocer la correspondencia que existe entre los objetos y las tablas lo hace por medio de la configuración de mapeo. Esta configuración se puede hacer de forma programática o bien, la más utilizada, que consiste en archivos de mapeo XML (mapping files). Estos archivos poseen la información necesaria para poder saber en qué tabla/s se tiene/n que guardar cada objeto ó en que tabla tengo que buscar para obtener un objeto. Los nombres de los archivos terminan con el sufijo .hbm.xml. Las extensiones de estos archivos xml es .hbm.xml. Por ejemplo Factura.hbm.xml, LineaFactura.hbm.xml, Producto.hbm.xml.

Archivo de Mapeo de la clase Factura (Factura.hbm.xml):

<?xml version="1.0" encoding="utf-8" ?>
	<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Dario.NH01" namespace="Dario.NH01.Entidades">
	
	  <class name="Factura">
		<id name="Id" column ="IdFactura" type="int" unsaved-value="0">
		  <generator class="identity"/>
		</id>
		<property name="Fecha" type="DateTime" not-null="true"/>
		<bag name="Lineas" cascade="all-delete-orphan">
		  <key column="IdFactura"/>
		  <one-to-many class="LineaFactura"/>
		</bag>
	  </class>
	</hibernate-mapping>

Veamos los atributos del archivo. Al comienzo del archivo estamos indicando el nombre del assembly: Dario.NH01, donde van a estar ubicadas las clases. Luego el namespace de dicha clase. Luego comenzamos a tratar las características de la clase que queremos mapear con el atributo class, en este caso Factura. Dentro de los tags <class name="Factura"...> ... </class> vamos a indicar todas las configuraciones de dicha clase. Un atributo que no hemos marcado en el mapping pero que viene activado por defecto y vale la pena explicarlo es lazy="true". Con esto activamos la carga perezosa, ó lazy load, de modo que las colecciones no se cargarán hasta que sean utilizadas. Este funcionamiento se logra mediante un proxy y NH se encarga de manejarlo, para nosotros es transparente (por ahora).

Con el tag <id> vamos a indicarle a NH qué property será mapeará con la clave primaria de la tabla, en este caso la property Id se va a corresponder con la columna IdFactura, será de tipo int y para indicar que un objeto se considera como nuevo y no como que ya existe en la base, pondremos el atributo unsaved-value="0". Tambien hemos asignado al generator el valor identity, con esto le decimos NH que se encargue de generar los valores de la primary key. Si hubiéramos querido asignarles nosotros el valor, deberíamos reemplazar por el valor assigned. Luego vamos a decirle que la propiedad Fecha, tiene que corresponderse con la columna Fecha, esto es debido que no hemos indicado el nombre de la columna con el atributo column, y que va a ser de tipo DateTime. Y con esto ya tenemos suficiente como realizar un ABM (Alta/Baja/Modificación) de la entidad Factura.

Pero para hacer un poco más interesante esto, agreguemos la relación 1:N con la clase LineaFactura. Para esto vamos a utilizar el atributo <bag>. Indicamos que el nombre de la colección se llamará Lineas, y como es bag, esta colección podrá tener elementos repetidos. Para esto utilizamos una colección del tipo IList<LineaFactura>. Cabe destacar que podemos utilizar otro tipo de colecciones que se encuentran en la librería Iesi.Collections.dll.

Archivo de Mapeo de la clase LineaFactura (LineaFactura.hbm.xml):

	<?xml version="1.0" encoding="utf-8" ?>
	<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Dario.NH01" namespace="Dario.NH01.Entidades">
	
	  <class name="LineaFactura">
	
		<id name="Id" column ="IdLineaFactura" type="int" unsaved-value="0">
		  <generator class="identity"/>
		</id>
	
		<property name="Cantidad" type="int" />
		<property name="Precio" type ="Decimal"/>
	
		<many-to-one name="Factura" column="IdFactura" />
		<many-to-one name="Producto" column="IdProducto" />
	
	  </class>
	</hibernate-mapping>

Algo a notar en este archivo es la declaración de la relación many-to-one para la propiedad Factura y también para Producto.

Archivos de mapeo como recursos embebidos

Es una práctica muy común y recomendable el incluir los archivos de mapeo como recursos embebidos dentro de un assembly. Cualquier IDE de desarrollo moderna -Sharp Develop, Visual Studio ó Monodevelop-, nos permiten configurar las propiedades de los archivos y que acción se debe tomar con ellos, dicha opción se llama Action Build / Acción de Construcción y entonces debemos configurarla como: Embedded Resource/Recurso embebido. Si olvidamos hacer esto con los archivos de mapeo, es posible que no tengamos ningún error en algunos casos, pero no funcionará correctamente.

Preparando las clases

Métodos y Propiedades

Al estar trabajando con clases con lazy="true", que es el valor por defecto, todos los métodos y propiedades deben ser declarados como virtual. De este modo NH puede crear un proxy de nuestras entidades, esto es transparente para nosotros.

Sobrescribiendo Equals y GetHashCode

Para poder trabajar correctamente con colecciones debemos sobrescribir estos métodos. Sino sobrescribimos el Equals al hacer una operación como lineaFactura1.Equals(lineaFactura2) podría devolver false aún tratándose de la misma linea de factura, debido a que la comparación se está haciendo por la posiciones de memoria. Cuando sobrescribimos Equals por ejemplo de esta manera:

			public override bool Equals(object obj)
			{
				if (this == obj) return true;
				LineaFactura lineaFactura = obj as LineaFactura;
				if (lineaFactura == null) return false;
				return id == lineaFactura.id && Equals(factura, lineaFactura.factura);
			}
	 

De este modo nos aseguramos de que 2 objetos son iguales si son iguales sus propiedades Id y Factura.

Ya hemos sobrescrito el Equals, ahora debemos hacer lo mismo con GetHashCode, debido a que 2 objetos iguales deben tener un mismo número de hash (y 2 objetos distintos pueden o no tener el mismo número de hash). El número hash no representa un id, no tiene que ser único.

Una posible implementación podría ser:

	  public override int GetHashCode()
			{
				return id + 29*factura.GetHashCode();
			}

Con este método nos aseguramos que 2 líneas factura devuelvan el mismo número al ser iguales. Al trabajar con números primos obtenemos buenas funciones hash. Lo importante aquí es hacer bien la función, la optimización de esta función queda fuera del alcance del tutorial.

Implementando IEquatable<T>

Todas las entidades implementan IEquatable<T>, esto no es necesario para trabajar con NHibernate, solamente es una manera de hacer que las comparaciones por medio de Equals se realicen de forma tipada.

ISessionFactory e ISession

Una sesión es un marco de trabajo en el cual NH establece una conversación entre la aplicación y el motor de base de datos relacional. Para construir una sesión, representado por ISession alguien que nos provea la sesión, para esto necesitamos de: ISessionFactory. El ISessionFactory se encarga crear sesiones en nuestra aplicación. En un momento, la aplicación puede tener 1 o más sesiones abiertas. Cada sesión representa un 1er. nivel de caché, los objetos que son traídos desde la base, o son guardados desde la aplicación se encuentran en la cache de primer nivel. Si se solicita un objeto a la base, primero se busca en la caché, si se encuentra ahí el objeto, se lo devuelve a la aplicación sin solicitarlo al motor relacional. Sino se encuentra en la caché, se realiza la consulta a la base.

Por lo general debemos tener un ISessionFactory por aplicación, sería necesario tener más de uno en el caso de que estemos trabajando con más de una base de datos a la vez.

Configuración del ISessionFactory de la aplicación

Para configurar el ISessionFactory, es decir, para decirle con qué motor de base de datos vamos a trabajar, la cadena de conexión (connection string), el driver que utilizaremos entre otras cosas, como toda configuración en NH, se puede realizar de manera programática (por código) o mediante archivos de configuración XML, y dentro de esta última podemos hacerlo mediando el App.config o mediante el hibernate.cfg.xml. Utilizaremos la última en este caso:

	<?xml version="1.0" encoding="utf-8" ?>
	<hibernate-configuration  xmlns="urn:nhibernate-configuration-2.2" >
	  <session-factory name="NH01">
		<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
		<property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
		<property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
		<property name="connection.connection_string">Data Source=localhost\SQLEXPRESS;Initial Catalog=NH01;Integrated Security=True</property>
		<property name="show_sql">true</property>
	   
		<mapping assembly="Dario.NH01" />
		   
	  </session-factory>
	</hibernate-configuration>
	

En esta configuración hemos incluido el nombre del assembly donde se encontrarán embebidos los archivos de mapeo, en este caso el assembly es: Dario.NH01.

El código necesario para configurar NH al comienzo de la aplicación es:

				Configuration cfg = new Configuration();
	
				cfg.Configure("hibernate.cfg.xml");
	
				ISessionFactory sesiones = cfg.BuildSessionFactory();
	
				ISession sesion = sesiones.OpenSession();

Primeramente debemos crear un objeto del tipo Configuration, configurarlo mediante Configure("hibernate.cfg.xml") y que se encargue de crear el ISessionFactory, por medio del método BuildSessionFactory(). Una vez que creamos el objeto sesiones del tipo ISessionFactory, ya podemos comenzar a crear objetos ISession. En este caso trabajaremos con una sola sesión en la aplicación: sesion.

Es una buena práctica realizar un singleton del objeto ISessionFactory debido a que es un proceso costoso para la aplicación, en otras palabras, la ejecución de BuildSessionFactory se debe hacer una vez durante la ejecución.

Creamos Productos, Facturas y Líneas

Ahora comenzamos con el código de ejemplo, primero creamos Productos y los guardamos:

		Producto prod1 = new Producto("Leche Entera", "Lacteos");
		Producto prod2 = new Producto("Lavandina", "Limpieza");
		Producto prod3 = new Producto("Vasos", "Bazar");
	
		sesion.Save(prod1);
		sesion.Save(prod2);
		sesion.Save(prod3);
	

Creamos 3 productos y los guardamos en la sesión sesion mediante el método Save. Una vez hecho esto podemos continuar con la creación de la Factura y sus LineaFactura.

		Factura factura = new Factura(DateTime.Now);
		factura.Lineas.Add(new LineaFactura(factura, prod1, 2.25m, 5));
		factura.Lineas.Add(new LineaFactura(factura, prod2, 3.5m, 1));
		factura.Lineas.Add(new LineaFactura(factura, prod3, 5.4m, 10));
	
		sesion.Save(factura);
	
		sesion.Flush();
	
		sesion.Close();	

Una vez creada factura, podemos realizar un Save . Notése aquí que hemos guardado la factura, y las líneas también se guardaron. Esto es debido a que en el mapping de Factura hemos configurado el Agregado/Actualización/Borrado en cascada mediante la siguiente línea cascade="all-delete-orphan". Después de realizar todos las inserciones, procedemos a realizar Flush() para que se ejecuten las sentencias SQL propiamente dichas contra el servidor, en ese momento se guardan todos nuestros objetos, antes de esto los objetos permanecían en el objeto sesion (en la cache de 1er. nivel).

Luego de que hemos usado la sesión, liberamos los recursos realizando un Close().

Intellisense para los archivos de mapeo en Visual Studio

Los archivos de mapeo de este ejemplo fueron escritos sin ayuda de una herramienta de generación de código. Solamente con ayuda del Intellisense de Visual Studio en los archivos XML. Para tener esta funcionalidad se deben copiar los archivos nhibernate-mapping-2.2.xsd y nhibernate-configuration-2.2.xsd al directorio C:\Archivos de programa\Microsoft Visual Studio 8\Xml\Schemas donde se tiene instalado el Visual Studio.

Conclusión

En este tutorial vimos nuestros primeros pasos con NHibernate mediante una simple aplicación de Consola. Se mostró como crear objetos y como persistirlos, inclusive como persistir una relación en cascada de padres e hijos (Factura - LineaFactura).

Consultas y comentarios