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.

36 comentarios:

  1. Hola!! En tu experiencia, cuáles son las contras de NHibernate y que cosas no puede hacer? Saludos!

    ResponderEliminar
  2. Hola Mauro, que tal.

    El punto mas flojo que veo esta referido a como trabaja con claves compuestas, la verdad este punto me ha dado bastante dolor de cabeza, especialmente si trabajas con diseños de bases existentes y queres modelizarlas en objetos lo mejor que se pueda.

    Recomendacion: implementar algun tipo de herencia con claves compuestas no te voy a decir imposible, pero casi.

    Lo importante aca es que tengas un poco de libertar para rediseñar la estructura de datos, si es asi, la verdad no tenes limite, ahora si la estructura de la db es fija, bueno ahi la cosa cambia un poco, y de seguro estes limitado en algunos aspectos.

    ResponderEliminar
  3. Hola Leandro!

    Gracias por el ejemplo, y felicitaciones por estrenar blog!

    Bien, ahora estoy trabajando, pero espero ver en mas detalle tu ejemplo. Me gusta que hayas encarado el tema de herencia, y de discriminador, asi como el campo clob.

    Nos leemos!

    Angel "Java" Lopez
    http://www.ajlopez.com/

    ResponderEliminar
  4. Hola Leandro

    Queria felicitarte por el excelente ejemplo que publicaste.

    Despues de mucho buscar, por fin encuentro un ejemplo bien hecho, con un muy buen diseño.
    Realmente fundamental para poder empezar a entender el tema de NH

    Pregunta, podrias explicar la diferencia entre el Flush y el Clear de una Session? y porque NH bloquea un objeto (Read Mode) cuando lo obtuvo, si si se quiere hacer un IList y asignar ese IList a una grilla, se produce un fallo de Reflection. Como que NH no permite acceder a objetos previamente cargados. Que habria que hacer, un Flush o un Clear antes de traer un List?

    No se si logré explicar el problema.

    Un fuerte abrazo y adelante !!!
    Daniel Laco

    ResponderEliminar
  5. Leandro, muy bueno tu ejemplo.

    Explicas muy bien los distintos casos/formas de mappings, etc. Voy a agregarlo en mi lista de links, referencias.

    Lo unico que no estoy muy de acuerdo es con el tema del Session Management, en WinForms.
    En tu caso estas usando static para la session lo cual no resulta muy practico en WinForms. (*)

    Yo acabo de arrancar un ejemplo (fijate en mi BLOG) donde trato de profundizar al menos en este tema.

    Nota: Mi ejemplo esta en pañales pero bienvenido cualquier comentario!

    ResponderEliminar
  6. Los Link que estan al principio no funcionan, donde puedo descargar esto?

    ResponderEliminar
  7. El enlace no funciona, es una pena porque tiene buena pinta

    ResponderEliminar
  8. Excelente post...con esto creo que entendere mejor este tema, que ya llevo con muchas ganas de aprender...
    Gracias.

    ResponderEliminar
  9. Hola...

    Estuve echando un vistazo al artículo, y me entró una duda. Se puede usar NHibernate para usar con compact framework?

    Me parece que no se puede, es así?

    ResponderEliminar
  10. hola

    Debo decirte que dudo mucho que puedas usar NHibernate en tu desarrollo mobile

    NHibernate on the .net compact framework

    NHibernate for .NET Compact Framework


    Ademas si lo piensas te daras cuanta de porque no se puede, mas alla que las dll de NHibernate puedas referenciarlas, este framework usa por debajo las librerias de ado.net para conectarse y trabajar contra la db, por ejemplo si desarrollas para conectarte contra Sql Server u Oracle estas librerias no podrias usarlas en tu desarrollo en compact framework.

    Algo que si podrias usar si te animas es db4o, esta es una base de datos orientada a objetos, si te gusta diseñar objetos y luego pesistirlos, similar a como harias con NHibernate, es una buena alternativa.


    db4o Compact Framework Database


    ademas es una db de libre uso

    saludos

    ResponderEliminar
  11. Los Link's de los ejemplos estan rotos, por favor podrias arreglarlos

    ResponderEliminar
  12. hola Marcelo

    Ya esta actualizado el link de descarga.

    Igualmente un comentario, te en cuenta que este codigo fue desarrollado con VS2005 y se uso UnitRun porque aun no se contaban con test en el VS, puede que este un poco desactualizado, aunque los conceptos de base de NHibernate aun deberia ser utiles.

    saludos

    ResponderEliminar
  13. saludos Leandro he leido tu articulo donde haces referencia al Inverse
    cito:"De no especificarse esta propiedad al momento de producirse, por ejemplo, la eliminacion de un objeto padre, sus hijos seran actualizados y al no encontrase la entidad superior generará una exception que indicara la imposibilidad de insertar null en el campo de la tabla. "

    Tengo una clase Persona y una clase Solicitud donde la primera tiene una lista de la 2da y en el mapeo uso HasMany. Si no uso inverse, en la tabla de solicitud en el campo de iD de Persona se ubican bien los respectivos id de Persona pero cuando intento borrar una persona me da un error que no me permite borrarla pues existe una referncia a ella en la tabla solicitud. Pero si le pongo el Inverse entonces cuando inserto la persona con su listado de Solicitudes en la tabla Solicitud en el campo Id de Pesona no se guarda el Id de la persona a las que esas solicitudes pertenecen. Por tanto de nada me sirbe la relacion. Alguna Idea????

    ResponderEliminar
  14. hola Banzai

    Suena logico lo que se produce en el primr planteo, si quiere eliminar una entidad drelacionada, salvo que habilites la eliminacion en cascada, esta bien que lance la exception.

    Ahora el segundo planeo es raro, habia que analizar como se relacionana nivel de objeto estas entidades, o sea la entidad Persona le has definido una propiedad de Solicitudes, para navegar esta entidfad y para agtregar nuevos items y persistir.

    La verdad estoy algo oxidado en el tema, hace tiempo que no trato con NHibernate, recomendaria si puedes plantear la consulta en el foro

    https://groups.google.com/group/nhibernate-hispano

    alli hay expertos que estan todo el dia con este ORM, seguro puedan aportar algo mas puntual

    saludos

    ResponderEliminar
  15. Muy chevere el ejemplo, pero tengo una inquietud, no se si es ignorancia o el formulario aspx no esta definido solo esta el default vacio , sera que lo tiene hecho.. es que soy nuevo y quiero ver como hacen el uso y el llamado de los metodos para que ingrese, modifique y elimine datos

    ResponderEliminar
  16. hola Juan D. Delgado

    la idea original del ejemplo era implementar todas las capas, pero como vi que se hacia largo solo quedo en los test

    unir las entidades a presentacion deberia ser un paso simple, el ejemplo solo apuntaba a la persistencia

    saludos

    ResponderEliminar
  17. Excelente aporte, por los modelos y por que sale andando al primer intento.
    Veo que es de hace unos años pero bajé el ejemplo, tiré dos lineas y salió andando sin ningún inconveniente.


    public void GuardarInstitucion(string descripcion, int cuit)
    {
    InstitucionEntity institucion = new InstitucionEntity();
    institucion.Cuit=cuit;
    institucion.Descripcion=descripcion;

    InstitucionRepository.Save(institucion);



    }

    ResponderEliminar
  18. consulta, al guardar siempre me pisa los datos de la tabla con los nuevos datos de la sessión, como puedo hacer algo tipo append en la base?

    ResponderEliminar
  19. hola Fer

    no entendi

    o sea quieres hacer un INSERT en una tabla?

    porque si es asi debes crear una nueva instancia de la entidad

    Class1 inst = new Class1
    //aqui completas propiedades

    y envias a persistir a inst

    saludos

    ResponderEliminar
  20. Hola Leandro.
    El inconveniente es que creo una nueva entidad y lo que ocurre cuando hago el persist/save es que agrega el registro por ejemplo con un id 1, cuando en la base ya existia un registro con el mismo id=1, e incluso un 2 y 3.










    lo único raro que tiene es que el id en la entidad viene de herencia de una clase entidad...


    ResponderEliminar
  21. incluso antes de hacer commit el valor del id ya cambió...es decir, toma el nuevo objeto como un update...
    ahora, si creo tres instancias, hago el save de una, luego de la otra, etc si inserta 3...
    raro

    ResponderEliminar
  22. hola

    como defines en el xml de mapeo el campo id que imagine es la key de la tabla, no?

    indicas a este campo como del tipo identity, o sea autonumerico
    o eras tu desde codigo que genera el secuencial

    saludos

    ResponderEliminar
  23. id name="ID">
    generator class="identity" />
    column name="ID" sql-type="int" not-null="true" />
    </id

    dejé incompletos los tags porque sino no se veia.
    Igual he probado con varias formas, incluso al instanciar con el valor id en null, pero lo que parece estar pasando es que al levantar la sesión o al persistir desconoce lo que ya está en la tabla pues arranca desde el primer id como si fuese una tabla sin registros.

    ResponderEliminar
  24. así como está entiendo que debería tener en la sessión todos los objetos de la tabla para que detecte un nuevo registro...

    ResponderEliminar
  25. Listo. ya vi que era.
    Cuando armé la clase que generaba la instancia de la session estaba manteniendo estaba recreando la estructura de la tabla en cuestión como en tu ejemplo.
    Muchas Gracias, buen blog!
    Saludos!

    ResponderEliminar
  26. Hola Leandro, quisiera hacerte una consulta. No se si este el espacio indicado, pero bueno, no conozco otro.
    Mi consulta es esta: Tengo 3 tablas, llamemoslas ventas, items e ingresos. Una venta tiene un conjunto de items.
    Un ingreso tiene un conjunto de items. Podria utilizar una FK en items para relacionarla tanto con ventas como
    con ingresos y mediante un campo que haga las veces de discriminante decidir si esa FK apunta a ventas o a
    ingresos? de ser posible esto, que permite extender esta FK a tantas tablas como quiera, como seria el mapeo
    en NH? Estos tipos de relaciones en la DB se me presentan habitualmente y la verdad no se como resolverlos...
    Gracias de antemano Leandro.

    ResponderEliminar
  27. hola MP

    Ese concepto tal como lo describes no se puede realizar a nivel de diseño de base de datos y por ende luego llevar al mapeo con NHibernate

    Lo que si podrias hacer es aplicar el concepto de herencia, o sea definir en la tabla de item que tipo se trata, si es un ingreso o una venta.

    Entonces las tablas de ventas, y la de ingresos, se relacionan ambas a la de items, esta ultima va a tener al menos 3 campos, el idventa (nullable), idingreso(nulable) y tipo (venta o ingreso), con eso se genera la relacion

    Igualmente me parece que seria aun mejor si creas dos tablas de items, un ItemsVenta y un ItemsIngreso relacionado con cada tabla de forma separada porque cada item tiene conceptos difetentes, digo no se, seria para analizar al menos

    saludos

    ResponderEliminar
  28. Hola Leandro...

    Estuve leyendo tu blog, especificamente el tema de los campos CLOB lo que en este momento me esta dando algunos dolores de cabeza.

    Te explico: tengo una aplicacion donde se usa NHibernate. Esta app recupera informacion de, y guarda informacion en, una base de datos Oracle 11g pero especificamente cuando se intenta actualizar un campo de cierta tabla con la data correspondiente en un campo en particular de una de las tablas de la base de datos se origina una excepcion debido a que no puede guardar ese valor porque el campo es LONG. Vale decir que la data que intento guardar es una cadena muy muy larga.
    Haciendo una inspeccion del mapping de la entidad dicho campo esta como STRING cuando en realidad en la base de datos Oracle el campo es CLOB.

    He buscado y buscado en internet una solucion pero no logro dar con algo concreto y ENTENDIBLE para poder implementarlo y probar.

    OJO: Soy principiante en NHibernate, apenas 3 meses, y de muchas cosas que he aprendido esta es una de las cosas que no he podido resolver.

    Pregunto: Como se resuelve ese problema?. Hay algun articulo que se parezca a mi problema que pueda leer o si necesitas el codigo con mucho gusto puedo pasar el mapping de la entidad y la definicion de la misma en C#.

    Saludos y espero puedas ayudarme...

    ResponderEliminar
  29. hola Dirimo

    lo que no me quedo claro es porque dices: intento guardar es una cadena muy muy larga.

    o sea si es una cadena no deberia ser numerico el campo, porque es un string
    habias revisado este link:
    Mapping a long text string in Oracle and NHibernate

    saludos

    ResponderEliminar
  30. Hola Leandro,

    Disculpa estoy iniciándome en esta tecnología de NHibernate y quiero trabajar con la versión 3.3.3 de NHibernate, este ejemplo puedo usarlo con esa versión?.

    Saludos gracias

    ResponderEliminar
  31. hola sadi++

    la version que utilice aqui es bastante antigua, seguramente los conceptos no hayan cambiado y se pueda seguir usando de base, pero creo que NHibernate aplico mejoras que deberias, o al menos seria recomendable analizar, como ser la configuracion por medio de fluent

    http://www.nuget.org/packages/FluentNhibernate

    creo que mapear por fluent en lugar de xml es muchisimo mejor

    NHibernate 3.0 Tutorial with Fluent NHibernate and Linq 2 NHibernate

    analiza si esto no te resulta util, digo ya que vas a usar una version mas actualizada de NHibernate

    actualmente estoy usando mas Entity Framework Code First, este se configura por fluent es es una maravilla

    saludos

    ResponderEliminar
  32. Buenas tardes Leandro.

    Me estoy iniciando en el desarrollo de aplicaciones con NHibernate, pero me surge una duda que no he podido resolver.

    Como almacenar un objeto en Caché, sin guardarlo aún en la base de datos, esto es porque tengo que desarrollar una aplicación que genere un objeto usuario y que el objeto usuario navegue en el sitio sin guardarlo en la base de datos hasta que haya terminado su participación entonces si deberá guardar su registro en base de datos. Quiero evitar tener que serializar el objeto para pasarlo entre las paginas ya que NHibernate es un ORM de persistencia, entonces me debería permitir almacenar el objeto en algún lugar del Caché de NHibernate sin guardar en la base de datos, esto es posible?????

    Gracias por tu ayuda.

    ResponderEliminar
  33. hola Carlos

    que tipo de aplicacion estas desarrollando, es web o desktop ?

    lo pregunto porque quizas no necesitas que sea NHibernate quien realice esto, sino que podrias usar la Session para conservar el usuario y accederlo desde diferentes paginas sin persistirlo

    saludos

    ResponderEliminar
  34. Hola Leandro, buen día, gracias por tu respuesta, mi aplicacion es web, por que no recomiendas persistir el objeto con nhibernate y como lo manejarias con sessiones???

    ResponderEliminar
  35. hola Carlos

    recuerda que Nhibernate trabaja con el concepto de session, o sea puedes mantener un objeto en cache pero mientras la session este activa, lo cual no se mentiene entre request

    si en el evento de un boton pones algo en la session de nhibernate pero no realizas flush para enviarlo a la tabla y se persista lo perderas para el siguiente request, porque imaginos la session no la mantienes entre request sino que creas una nueva, por eso el cache solo dura lo que el request este activo

    podrias quizas evaluar
    Habilitar cache de segundo nivel en NHibernate


    NHibernate Second Level Caching Implementation


    pero bueno habria que ver si serian util en este caso que planteas

    Nota: cuando mencione Session no es la de asp.net, apuntaba a la de nhibernate

    saludos

    ResponderEliminar
  36. Carlos

    podrias plantear la duda aqui

    NHibernate foro

    quizas tengan alguna otra idea de como manejar los datos en este caso que planteas

    saludos

    ResponderEliminar