Recientemente me he visto la VAN de Alt.Net Hispano sobre estrategias de uso de la sesión con nHibernate, en el que Nelo Pauselli (@nelopauselli) nos habla de la importancia de manejar correctamente las sesiones para que nuestra aplicación no comience a tener comportamientos extraños.

Voy a resumir con ejemplos las características y comportamientos más importantes de las sesiones de nHibernate que he aprendido. Para los ejemplos usaré el framework de test unitarios xUnit y C#.

¿Qué es una sesión de nHibernate?

En pocas palabras, podríamos decir que es el marco de trabajo que nos ofrece nHibernate en el cual se establece una conversación entre la aplicación y la base de datos para consultar y persistir información. Internamente implementa el patrón UnitOfWork, al igual que hace el ObjectContext de EntityFramework.

La sesión también nos provee de un primer nivel de cache en el que si pedimos un objeto de base de datos que previamente fue cargado en la sesión, nHibernate devolverá el objeto cacheado y no hará una nueva consulta a la base de datos. En nuestra aplicación podemos abrir tantas sesiones como necesitemos y cada una tendrá su propia cache de primer nivel.

La clase que usaremos para crear nuevas sesiones será la SessionFactory, de la cuál sólo tendremos una instancia para toda la aplicación.

Síntomas del mal manejo de la sesión en nuestra aplicación

En muchas ocasiones los desarrolladores echan la culpa a los framework por el mal comportamiento o rendimiento de sus aplicaciones. En ocasiones tendrán razón, pero si se trata de un framework de calidad, como es el caso de nHibernate, seguramente el problema radicará en el mal uso o en el desconocimiento del propio framework. Con nHibernate, los problemas más típicos que suelen florecer son:

  • Al intentar acceder a ciertas propiedades de una entidad cargada se lanza una excepción del tipo LazyLoadingException.
  • Persistencia extraña, a veces no se persisten datos o se duplican datos en memoria.
  • La aplicación no funciona bien con más de un usuario simultáneo.

La mejor forma de combatir estos problemas es conocer el funcionamiento de la sesión de nHibernate. Para ello, vamos a ver algunas de sus principales características.

Por defecto, Lazy Load

El funcionamiento por defecto de nHibernate en cuanto a la carga de entidades asociadas a una entidad (sus relaciones con otras entidades) es mediante la carga baga. Esto significa que, si tengo una entidad “categoría” y esta tiene asociada una “categoría padre”, nHibernate cargará de la base de datos la categoría padre en el momento en el que intentemos acceder a una de sus propiedades. Para que esto sea posible es necesario mantener abierta la sesión que se usó para cargar la categoría padrea, de lo contrario recibiremos una excepción del tipo LazyLoadingException. Veamos un ejemplo:

[Fact]
public void LazyLoadConSessionCerradaNoFunciona()
{
    ProductCategory category;
    using (ISession session = SessionFactory.OpenSession())
    {
        category = session.Get<ProductCategory>(8);
    }
    Assert.Throws<LazyInitializationException>(() => category.ParentProductCategory.Name);
}

Este test demuestra como nHibernate lanza la excepción si intentamos acceder a la categoría padre una vez la sesión ha sido cerrada.

Otra forma de trabajar con la carga de entidades relacionadas es mediante la carga temprana de las mismas (eager loading). La carga baga de entidades es un arma de doble filo ya que puedes tener problemas de rendimiento dependiendo de tu escenario. Puedes leer más sobre la carga temprada de entidades (eager loading) con nHibernate en mi anterior post.

La caché de la sesión

Cada sesión de nHibernate que abras, tiene su propia cache de objetos. Si un objeto es cargado por una sesión, la próxima vez que se intente cargar ese mismo objeto en la sesión se obtendrá de la caché, sin que sea lanzada una consulta a la base de datos. Sólo hay una instancia de un objeto para una misma entidad en una misma sesión en memoria. En el caso que intente cargar esa misma entidad, pero en otra sesión, si que se realizaría una nueva consulta a la base de datos y tendría en memoria dos objetos de la misma entidad, pero cada uno en una sesión diferente.

Este aspecto es muy importante tenerlo en cuenta suele ser el principal quebradero de cabeza con nHibernate cuando no se aplica la estrategia de uso de la sesión adecuada a tu aplicación. Nelo Pauselli realizó un webcast sobre las diferentes estrategias de uso de la sesión en diferentes entornos como son las aplicaciones Windows Forms, WPF, WCF, Servicios, ASP.NET y ASP.NET MVC.

Vamos a ver unos ejemplos de código para entender mejor esta característica:

[Fact]
public void MismaEntidadPadreEnSesionesDistintasSonDiferentes()
{
    ProductCategory category1;
    ProductCategory category2;
    using (ISession session = SessionFactory.OpenSession())
    {
        category1 = session.Get<ProductCategory>(8);
    }
    using (ISession session = SessionFactory.OpenSession())
    {
        category2  = session.Get<ProductCategory>(8);
    }
    Assert.NotEqual(category1, category2);
}

En este ejemplo, hemos cargado la misma entidad en diferentes sesiones de nHibernate y lo que obtenemos son dos objetos de la misma entidad en memoria. Cada uno de ellos se encuentra en la cache de una sesión diferente.

[Fact]
public void MismaEntidadHijaEnSesionesDistintasSonDiferentes()
{
    //NOTA: La categoría 8 y 9 tienen el mismo padre asociado

    ProductCategory parent1;
    ProductCategory parent2;
    using (ISession session = SessionFactory.OpenSession())
    {
        ProductCategory productCategory = session.Get<ProductCategory>(8);
        parent1 = productCategory.ParentProductCategory;
    }
    using (ISession session = SessionFactory.OpenSession())
    {
        ProductCategory productCategory = session.Get<ProductCategory>(9);
        parent2 = productCategory.ParentProductCategory;
    }
    Assert.NotEqual(parent1, parent2);
}

Aquí podemos observar como a la misma categoría padre de diferentes entidades en diferentes sesiones también obtenemos dos objetos de la misma entidad en memoria.

En cambio, si escribimos este mismo ejemplo pero usando para la carga la misma sesión, el resultado es diferente:

[Fact]
public void MismaEntidadHijaEnMismaSesionSonIguales()
{
    //NOTA: La categoría 8 y 9 tienen el mismo padre asociado

    ProductCategory parent1;
    ProductCategory parent2;
    using (ISession session = SessionFactory.OpenSession())
    {
        ProductCategory productCategory = session.Get<ProductCategory>(8);
        parent1 = productCategory.ParentProductCategory;

        ProductCategory productCategory2 = session.Get<ProductCategory>(9);
        parent2 = productCategory2.ParentProductCategory;
    }
    Assert.Equal(parent1, parent2);
}

En este caso, sí que obtenemos la misma referencia al objeto de la categoría padre.

Una sola sesión para toda la aplicación, generalmente mala idea

En este momento se te puede estar ocurriendo que la solución al problema de las caches y las sesiones pueda ser usar una única sesión para toda mi aplicación. De esta forma me olvido de tener varios objetos de una misma entidad en memoria, por ejemplo. Generalmente no es la solución correcta para una aplicación por muchos motivos. Veamos un ejemplo de lo que nos puede ocurrir:

[Fact]
public void EntidadModificadaPedidaEnLaMismaSesionDevuelveLaEntidadModificada()
{
    ProductCategory category1Session1;
    string category1Session1OriginalName;
    ProductCategory category2Session1;
    ProductCategory category3Session2;

    using (ISession session1 = SessionFactory.OpenSession())
    {
        category1Session1 = session1.Get<ProductCategory>(8);
        category1Session1OriginalName = category1Session1.Name;
        category1Session1.Name = "new name";

        category2Session1 = session1.Get<ProductCategory>(8);
    }
    using (ISession session2 = SessionFactory.OpenSession())
    {
        category3Session2 = session2.Get<ProductCategory>(8);
    }

    Assert.NotEqual(category1Session1OriginalName, category2Session1.Name);
    Assert.Equal(category1Session1.Name, category2Session1.Name);
    Assert.Equal(category1Session1, category2Session1);

    Assert.Equal(category1Session1OriginalName, category3Session2.Name);
    Assert.NotEqual(category1Session1, category3Session2);
}

Nos encontramos con dos sesiones. En la primera cargamos la categoría 8, le modificamos el nombre y la volvemos a cargar en otra variable. Como estamos en el ámbito de la misma sesión, en el primer intento de carga se consulta a la base de datos pero en el segundo intento, nHibernate nos devuelve en objeto de la caché. Nos encontramos con que tenemos dos variables (category1Session1 y category2Session1) que son referencias al mismo objeto en memoria, por lo que category2Session1 tiene como nombre “new name” y no el nombre original de la base de datos.

En cambio, la misma categoría cargada en la segunda sesión, si que realiza una nueva consulta a la base de datos y obtenemos un nuevo objeto de la entidad en memoria.

Este problema suele florecer en aplicaciones con usuarios concurrentes. Las estrategias del ciclo de vida de las sesiones más utilizadas en aplicaciones son:

  • Session per request, para aplicaciones asp.net
  • Session per action. para aplicaciones asp.net mvc
  • Session per form, para aplicaciones windows forms y wpf
  • Session per call, para aplicaciones de servicios
  • Session per invoke, para servicios WCF
  • Conversation per bussiness trasaction, para aplicaciones web y windows

Para más información sobre estas estrategias, te recomiendo que veas el webcast de Alt.Net Hispano.

Otras características de las sesiones a tener en cuenta

Cuando trabajes con nHibernate, ten en cuenta lo siguiente:

  • Crear una nueva sesión de nHibernate no implica el abrir siempre una nueva conexión a la base de datos.
    Este trabajo con el pool de conexiones lo gestiona internamente nHibernate.
  • La sesión de nHibernate no es Thread Safe.
    Esto significa que no debes de compartir la sesión entre diferentes hilos de tu aplicación. Lo único que es Thread Safe es el SessionFactory.
  • Si una sesión lanza una excepción, se vuelve inestable y lo mejor es reemplazarla por una nueva.
    No es recomendable seguir usando una sesión que ha generado una excepción. Imagínate si en una aplicación Windows Forms o WPF usaras una única sesión para toda tu aplicación. En cuanto la sesión lanzara una excepción de cualquier tipo (consulta SQL, problemas de conexión a la base de dato, etc), toda tu aplicación se volvería inestable hasta que se reiniciara o crearas una nueva sesión.