NHibernate ejemplo práctico de Lazy=Extra y otras yerbas

Disclaimer: En este post puedo estar muy equivocado en algunas cosas (al igual que en todo lo que escribo), por lo tanto déjame un comentario, no seas amarrete.

Voy a plantear este ejemplo, con un dominio que esta relacionado a mi trabajo:

ClassDiagram1

La empresa que paga mi alimento, realiza muchas operaciones al exterior por lo tanto se hace necesario disponer en la base de datos de su sistema, una estructura que permita almacenar cotizaciones de las diferentes monedas. La entidad Cotización es relativamente sencilla, tiene dos propiedades fundamentales que son el tipo de cambio para la Compra y el tipo de cambio para la Venta, Fecha es fundamental también, puesto que la cotización que se registra es válida para una determinada fecha (y para todas las fechas existe una cotización).

Ahora bien la entidad Moneda en principio tenía solamente la propiedad Nombre, y fue ayer que se me ocurrió bueno agregar la propiedad Cotizaciones y del tipo IDictionary cuya clave del diccionario es un DateTime (que equivale a la fecha de cotización).

Típicamente esto sería muy dañino puesto que cotizaciones puede haber al rededor de:
10 años x 365 días aprox. = 3650 cotizaciones para una determinada moneda. Y ya sea que estés usando Eager o Lazy loading, traer 3650 registros desde la base es dañino. Por lo tanto la solución típica sería no trasladar esto al modelo de objetos, a pesar de que a mi parecer sería un buen diseño orientado a objetos.

Afortunadamente estoy utilizando NHibernate como ORM, y esta herramienta tiene una opción Lazy=”Extra” (a mi criterio Extra no me dice nada, pero no se cual sería un mejor nombre). Lo que hace esta opción es solo hacer fetch del elemento que se necesita siempre tratando de evitar obtener toda la collection. Desde ya si nosotros enumeramos toda la collection, lo va a hacer.

Basta de palabras y a ver el código:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="EjemploLazyExtra"
                   namespace="EjemploLazyExtra">

    <class name="Moneda">
        <id name="Id">
            <generator class="hilo"/>
        </id>
        <map name="Cotizaciones" cascade="none" lazy="extra">
            <key column="Moneda" />
            <index column="Fecha" type="DateTime" />
            <one-to-many class="Cotizacion" />
        </map>
        <property name="Nombre"/>
    </class>
    
  <class name="Cotizacion">
        <id name="Id">
            <generator class="hilo" />
        </id>
      
    <many-to-one name="Moneda" class="Moneda"  column="Moneda"/>
    <property name="Fecha" />
    <property name="Compra" />
    <property name="Venta" />
  </class>
</hibernate-mapping>

Notar como se mapea el “map” de cotizaciones y el uso de la opción lazy=”extra”.

Prueba de concepto

Cómo pruebo yo este concepto?, puede aparecer alguna gente que diga que es fácil con NHProf. En serio? Yo no lo usaría para muchas de las cosas que la gente lo esta usando ahora y creo que si uno escribe buenos tests en muchos casos no lo necesitaría, este es mi test unitario:

[Test]
public void al_obtener_cotizacion_de_moneda_solo_debe_cargar_dos_entidades()
{
    var idMoneda = crear_datos_de_ejemplo();
    sessions.Statistics.Clear();

    using(var s = sessions.OpenSession())
    using(var tx = s.BeginTransaction())
    {
        var moneda = s.Get<Moneda>(idMoneda);
        moneda.Cotizaciones[new DateTime(2009, 4, 5)].Compra.Should().Be.EqualTo(4.04m);

        
        s.Statistics.EntityCount.Should().Be.EqualTo(2);

        tx.Commit();
    }
    
}

“Crear datos de ejemplo” es un método bastante trivial que genera 1 moneda y 100 cotizaciones sobre esa moneda, la fecha es incrementada 1 día para cada cotización, y el tipo de cambio y compra son incrementados 1 centésimo. Luego, lo que estoy haciendo es obtener una moneda, y de esa moneda obtener la cotización correspondiente a la fecha 05/04/2009, verifico que la cotización sea igual a 4.04 (m de decimal).

Lo importante de esta prueba de concepto es verificar que solamente se hayan cargado dos entidades y no 101. Para eso he utilizado las estadísticas de la session de NHibernate, existe una que se llama EntityCount que me dice “cuantas entidades hay relacionadas a esta session?”.

Como también he habilitado a NHibernate para que muestre el sql que esta ejecutando, podemos comprobar el correcto funcionamiento en la consola de nuestro test runner:

--primer consulta:

SELECT moneda0_.Id     AS Id0_0_,
       moneda0_.Nombre AS Nombre0_0_
FROM   Moneda moneda0_
WHERE  moneda0_.Id=@p0;

@p0 = 98304

--segunda consulta:
SELECT cotizacion0_.Id     AS Id1_0_    ,
       cotizacion0_.Moneda AS Moneda1_0_,
       cotizacion0_.Fecha  AS Fecha1_0_ ,
       cotizacion0_.Compra AS Compra1_0_,
       cotizacion0_.Venta  AS Venta1_0_
FROM   Cotizacion cotizacion0_
WHERE  cotizacion0_.Moneda=@p0
AND    cotizacion0_.Fecha =@p1;

@p0 = 98304, @p1 = 05/04/2009 0:00:00

Último tip:

En mi camino al aprendizaje de NHibernate me he encontrado en la situación de descargar muchos ejemplos. Y en algunos casos como este, dichos ejemplos no tenían base de datos adjunta, suelo adjuntar en mis ejemplos un test case como este:

[Test, Ignore]
public void createschema()
{
    var config = new NHibernate.Cfg.Configuration();
    config.Configure();
    var schema = new SchemaExport(config);
    schema.Execute(true, true, false);
}

Este test es ignorado completamente por el test-runner a menos que lo ejecutemos explícitamente. Al ejecutarlo, simplemente creara el schema en la base de datos que hayas configurado en el archivo hibernate.cfg.xml. Cuidado; no creará la base por si solo, solo creara el schema.

El ejemplo se puede descargar en este enlace y pueden jugar a poner lazy en true y false y ver los errores que arroja, así como también las sentencias ejecutadas contra el motor.

Saludos y como siempre digo, espero que les sea útil.


blog comments powered by Disqus
  • Categories

  • Archives