viernes, 28 de noviembre de 2008

NHibernate, ejemplo práctico

Para abrir el blog no elegí mejor tema que el grandioso framework de persistencia, de cual puedo estar hablando sino es NHibernate.

La idea principal de esta publicación será ni más ni menos que proporcionar un ejemplo integral de varias características que mucha falta hacen y que tanto cuesta aprender al comenzar con esta herramienta.

Este artículo no tiene intención de ser detallista ni explicar puntualmente cada aspecto implementado, para ello podrá encontrar artículos mas destacados como por ejemplo el excelente articulo publicado en el Blog de Dario Quintana.

La idea final es simplemente proporcionar un ejemplo que permita ver varias características funcionando juntas.

Aspectos implementados

Entre las características implementadas encontraran los siguientes puntos:

  • Modelo completamente tipado
  • Relaciones many-to-one, one-to-many y many-to-many
  • Persistencia de enumerados
  • Persistencia de campos nText (type="StringClob")
  • Herencia table per class hierarchy <subclass>, table per subclass <joined-subclass>
  • Herencia table per class hierarchy <subclass>, utilizando una formula en el <discriminator>.
  • Consultas hql entre relaciones many-to-many

[C#]
 

Diagramas del Dominio

Diagrama del dominio de la Institución

Diagrama del dominio de reserva de libros

Diagramas de persistencia en SQL Server 2005

Diagrama de Base de Datos de la Institución

Diagrama de Base de Datos de las Reservas de libros

Persistencia de Enumerados

Un punto mas que importante para brinda consistencia al modelo, es la utilización de enumerados en al definición del dominio de la aplicación.

En la clase ClaseEntity, se observara un atributo de nombre FormaPago definido como un enumerado, por supuesto en la definición de su mapper (Clase.hbm.xml) encontraran la definición de de la propiedad

<property type="NHib.Infrastructure.GenericEnumMapper`1[[NHib.Domain.Entities.enumFormaPago, NHib.Domain]], NHib.Infrastructure" name="FormaPago" column="FormaPago" />

Se observa claramente la utilización de un mapper de enumerado (el cual esta alojado dentro del NHib.Infrastructure), el cual permitirá recuperar el número asignado al ítem del enumerado asignado.

Persistencia de campos nText

Un tema interesante para probar es la persistencia de atributos espaciales, como es el caso de comentarios, los cuales deben contener texto que escapa a un simple descripción. Es en estos casos que se utilizara en nText, existiendo en NHibernate una forma especial de definir este tipo de mapeo.

En la clase LibroEntity, es justamente donde se hace uso de este, en el atributo Comentarios definido en el mapper como:

<property column="Comentario" type="StringClob" name="Comentario" />

Es en la clase de test TestLibro.cs -> LargoComentarios(), donde se realiza una prueba de su correcta persistencia, comprobando que no se trunca el valor del atributo

Herencia

En la aplicación encontraran tres tipos de herencia implementada, utilizando:

Tabla para todas las clases

Este primer tipo de implementación lo ubicaran en la entidad LibroEntity, en esta se remarca la definición de los atributos discriminator-value el cual toma un valor numérico cuyo tipo es definido en el tag <discriminator>

Se aclara que el tipo de dato por defecto definido es del tipo de datos string, esto a simple vista pareciera no tener mayor implicancia, pero al momento de utilizar un <discriminator> con valores numéricos, todas las clases deberán tener definido un discriminator-value, aunque la clase principal no sea utilizada.

En el ejemplo la definción del tag <class>, al estar utilizando la interface ILibroEntity, debe definir un discriminator-value, con un valor que no es utilizado por el modelo como diferenciador en la herencia de clases, esto debe hace con el simple objetivo de evitar un error de tipos de datos.

Tabla para cada subclase

La implementación se podrá observar en la clase ReservaEntity, a diferencia del ejemplo anterior que utiliza una sola clase, no existe el discriminator-value, y es por medio de la relación uno a uno de las tablas que se mapean como se obtiene el tipo de objeto.

En este caso se hará uso de un tag de mapeo: <joined-subclass>, la cual definirá cual es el atributo relación entre las tablas, siendo esta la razón de utilizar <key column="" />

Herencia utilizando formulas en el discriminator

Esta será utilizada en los casos en donde se pretenda generalizar entidades que pueden ser agrupadas desde otro nivel de abstracción que requiere el modelo analizado.

En el ejemplo sucedió con los convenios, (definido en la entidad ConvenioEntity) el cual describe la existencia de varios tipos de talleres y excursiones, que se pueden asociar a una institución.

Pero el modelo solo necesitaba diferenciar entre convenios del tipo taller y excursión, para esto necesita agrupar distintos valores en un discriminator.

Además para aumentar la complejidad el atributo esta definido con un carácter mientras que el mapeo hará uso de un valor numérico.

Es por medio del atributo formula que se transformara el valor del discriminator de una valor del tipo carácter a uno numérico.

Se debe aclarar que este tipo de definiciones presentan un inconveniente, solo podrán recuperarse los distintos tipos de subclases definidas, pero no podrán persistirse, ya que es imposible por medio de la definición de la formula saber que atributo de tipo especificar, esto deberá realizarse implícitamente en la definición de la clase a persistir.

Es por esta razón que se notara la definición de un enumerado dentro de las subclases que permita diferenciar las distintas clases concretas de Convenios. La redefinición del constructor permite forzar la especificación al momento de crear una nueva subtipo de Convenio.

El framework necesita la especificación de un constructor sin parámetros, pero para evitar su utilización es que se define como protected. Al igual que la definición del atributo Tipo (también protected) que permitirá ser utilizado solamente por el mapper, permitiendo especificar el tipo concreto a persistir.

Además se debe entender que por tratarse de un campo del tipo char, se realiza por medio del “case” la asignación del valor correcto, si hubiera sido numérico se podría haber persistido el enumerado directamente como el caso visto en a persistencia de enumerados.

Consultas hql entre many-to-many

Si bien la definición del mapeo de la navegación entre entidades resulta de suma utilidad, hay ocasiones en donde se requiere obtener algún tipo de filtro aplicado al otro extremo de la relación, es en este punto donde se hará uso de las consultas HQL, y en ellas el operador JOIN.

Para visualizar un ejemplo de su implementación se podrán visitar varios puntos donde fueron utilizados, destacando:

  • ProfesorRepository -> GetAlumnos(): Implementa un join en donde se recorren dos relaciones muchos a muchos. Esta podrá ser testeada desde TestProfesores -> RecuperoAlumnos()
  • AlumnoRepository -> GetByClase(): Define varias sobrecargas con diferente filtros que son aplicados a un join.

Eliminar en cascada

Como es de suponer esta facilidad de actualizar la cascada de elementos relacionados es de muchísima importancia, pero durante la utilización de la navegación entre entidades me encontré con un pequeño detalle que quería remarcar.

Se trata de la utilización de la propiedad inverse="true", esta es de suma importancia cuando se esta utilizando navegación bidireccional.

De no especificarse esta propiedad al momento de producirse, por ejemplo, la eliminación de un objeto padre, sus hijos serán actualizados y al no encontrase la entidad superior generará una excepción que indicara la imposibilidad de insertar null en el campo de la tabla.

Esto podría resolverse fácilmente marcando el campo de relación para que permita null en su contenido, pero no todos los tipos de relaciones en el modelo pueden hacer esto.

Un ejemplo del uso de esta propiedad podrá encontrase en el achivo de mapeo de la entidad Institución (Institucion.hbm.xml), y la prueba de la eliminacion en cascada se encuentra en el test: TestInstituciones -> EliminoInstitucion()

Nota: Si quieren probar como funciona esta propiedad elimínenla del archivo de mapeo Institucion.hbm.xml, de la relación con el Alumno por ejemplo, e intenten correr el test: TestInstituciones -> EliminoInstitucion(), para ver los resultados.

También podrán marcar el campos "Institución" de la tabla "Alumnos", observarán como se soluciona el problema.

Implementación

En la aplicación encontraran que esta formada por varias capas entre ellas la de Aplicación y Presentación, pero estas no están implementadas, pues no era necesario para el objetivo de aprender NHibernate, simplemente con la utilización del proyecto de Test se pudo comprobar el correcto funcionamiento del framewok de persistencia.

Se debe remarcar que el dominio debe ser tomado algo inventado, no perteneciente a un dominio existente, pensado simplemente para lograr el objetivo principal de verificar la funcionalidad de NHibernate.

Si bien el código es sumamente útil con tan solo visualizar el código de los test, así como también el de los mapper, es posible ejecutarlo si se conecta apropiadamente a una base de datos. Para ellos, como en la mayoría de las aplicaciones, simplemente se debe modificar el app.config del proyecto de pruebas, apuntándolo a la DB que corresponda.

Para crear la estructura de la base de datos se encontrar un proyecto con los script de creación de la estructura, o en caso contrario podrán hacer uso de los archivos .mdf y .ldf de la base de datos, adjuntándolos por medio de la opciones de attach del SQL Server 2005.

Para la ejecución de los test hice uso de UnitRun, de esta forma podía ejecutarlos uno a uno en modo debug e ir analizando como se comporta cada uno.

Conclusión

Espero antes que nada haber aportado un granito mas de arena a la investigación de este potentísimo framework, apuntando principalmente aspectos un poco mas avanzados de los cuales es complicado encontrar ejemplo cerrados que los integre.

Quedan varios puntos todavía por probar, como ser el mapeo a store procedure, paginado, etc

Espero les sea de utilidad y cualquier duda, modificación o error que encuentren serán bienvenidos.