Session-Per-Request en NHibernate con Action Filters en ASP.NET MVC

Una de las buenas prácticas a tener en cuenta antes de trabajar con NHibernate es la siguiente: La sesión y transacción pertenecen a un mismo ciclo de vida. Con esto quiero decir que es importante abrir una transacción cada vez que trabajamos con una base de datos dentro de una aplicación, ya sea para realizar una consulta, como una actualización, eliminación, etc. 

Dado que en el post anterior, donde comencé a montar una aplicación con NHibernate y MVC, realicé los ajustes básicos para poder instanciar una sesión y dejar una clase, Connection.cs, lista para su uso, ahora voy a mostrar una forma adaptada a la estructura de MVC para manejar tanto la sesión como la transacción. 

Llegados a este punto necesitamos conocer un nuevo concepto: Action Filters.
Un action filter es un atributo que podemos asignar tanto a una acción de nuestro controlador como a un controlador entero para poder proporcionarle una funcionalidad adicional. Para crear un atributo de tipo Action Filter, debemos generar una clase y heredar de ActionFilterAttribute. Esta clase contiene los siguientes métodos, ejecutados en este orden: 

  1. OnActionExecuting, se lanza antes de que la acción sea ejecutada.
  2. OnActionExecuted, es llamada después de que la acción finalice.
  3. OnResultExecuting, comienza antes de que el resultado sea devuelto a la página.
  4. OnResultExecuted, ocurre cuando ha terminado de ejecutar el resultado que generó la acción.

En este caso, Session-Per-Request se podría traducir en Session-Per-Action  ya que, lo que realmente nos interesa, es tener una sesión disponible para una acción en concreto o para todas las acciones de un controlador. Además, me interesa que la sesión de NHibernate esté disponible ANTES de realizar cualquier operación dentro de la acción en cuestión, ya que si no es así lo más probable es que se lanzara una excepción al no tener un objeto session listo al que consultar. Por último, para que el ciclo de vida se complete correctamente, cuando la acción solicitada finalice, necesito que mi transacción sea completada y mi sesión finalice junto con el ciclo de vida de la petición. Teniendo presentes estas anotaciones, podríamos obtener lo siguiente: 

using System.Web.Mvc;
using NHibernate.Context; 

namespace MovieManager.Models
{
    public class SessionPerRequest : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext1)
        {
            var session = MvcApplication.SessionFactory.OpenSession();
            session.BeginTransaction();
            CurrentSessionContext.Bind(session);
        }
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var session = CurrentSessionContext.Unbind(MvcApplication.SessionFactory);
            if (session != null)
            {
                if (session.Transaction.IsActive)
                {
                    try
                    {
                        session.Transaction.Commit();
                    }
                    catch
                    {
                        session.Transaction.Rollback();
                    }
                } 

                session.Close();
            }
        }
    }
}

Para este ejemplo, he sobrescrito los métodos OnActionExecuting y OnResultExecuted . En el primero de ellos, inicializo la sesión de NHibernate y bindeo la misma en el contexto de la aplicación. Por el contrario, en el segundo método recupero la sesión, compruebo si tiene alguna transacción en curso, realizo el commit (Si ocurriera algún error durante el commit de la transacción actual, se realizaría un rollback para evitar problemas) y cierro la sesión. 

En el archivo web.config, necesitamos declarar una propiedad para indicar el contexto en el que se enmarca la aplicación. Esta propiedad es current_session_context_class y , en este caso, podríamos declararlo como web, tal y como indico en el siguiente código: 

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
    <session-factory>
        <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider, NHibernate</property>
        <property name="connection.connection_string">
    Server=localhostsqlexpress;initial catalog=MovieManager;Integrated Security=true
  </property>
        <property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property>
        <property name="current_session_context_class">web</property>
        <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
        <mapping assembly="MovieManager"/>
    </session-factory>
</hibernate-configuration>

Por otro lado, podemos comprobar que hacemos referencia a MvcApplication.SessionFactory,  lo cual significa que, para tener un mayor acceso a nuestra “Factoría de sesiones” he posicionado su declaración en el archivo Global.asax para poder acceder desde cualquier parte de mi aplicación. 

using System.Web.Mvc;
using System.Web.Routing;
using NHibernate;
using NHibernate.Cfg;

namespace MovieManager
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode,
    // visit <a href="http://go.microsoft.com/?LinkId=9394801">http://go.microsoft.com/?LinkId=9394801</a>

    public class MvcApplication : System.Web.HttpApplication
    {
        public static ISessionFactory SessionFactory = new Configuration().Configure().BuildSessionFactory();

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
            );

        }

        protected void Application_Start()
        {
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

Para ir finalizando, podemos ver la forma de utilizar el atributo SessionPerRequest en un controlador donde necesitaremos persistencia. Colocamos el mismo entre corchetes bien en la declaración del controlador o en cualquiera de las acciones.

using System.Web.Mvc;
using MovieManager.Models;

namespace MovieManager.Controllers
{
    [SessionPerRequest]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var movieRepository = new MovieRepository(MvcApplication.SessionFactory);
            return View(movieRepository.ListMovies());
        }

        public ActionResult About()
        {
            return View();
        }
    }
}

En la acción Index, creamos un objeto MovieRepository, donde le pasamos como parámetro el objeto SessionFactory declarado en el fichero Global.asax. Llegados a este punto debemos tener en cuenta de que la sesión de NHibernate, necesaria para llamar a la base de datos y recuperar el listado de películas, ya ha sido inicializada justo antes de entrar en la acción gracias al atributo SessionPerRequest. Si echamos un vistazo al repositorio, vemos lo siguiente:

using System.Collections.Generic;
using MovieManager.Models.Objects;
using NHibernate;

namespace MovieManager.Models
{
    public class MovieRepository
    {
        private readonly ISessionFactory _session;

        public MovieRepository(ISessionFactory sessionFactory)
        {
            _session = sessionFactory;
        }

        public IList<Movie> ListMovies()
        {
            return _session.GetCurrentSession().CreateCriteria(typeof(Movie)).List<Movie>();
        }
    }
}

A través del constructor de la clase MovieRepository, estamos inyectando el objeto SessionFactory. De tal manera que podemos utilizar la misma factoría de sesiones en cualquiera de los métodos implementados en esta clase, que necesiten hacer una consulta, actualización, etc. Cuando todo haya terminado, todo quedará perfectamente finalizado gracias a Action Filters y Session-Per-Request/Action.

¡Saludos!

NHibernate y ASP.NET MVC

NHibernate es la versión de Hibernate (Java) para .NET. Es una herramienta para el mapeo objecto-relacional que ayuda a identificar en nuestro código los atributos de una clase con las columnas de nuestras tablas, por decirlo de una forma simplificada.
El objetivo es que, para el programador, sea posible manejar su base de datos a través de los objetos de .NET, con lo que se consigue una mayor abstracción.
Podemos conseguir más información en la página oficial de la comunidad de usuarios. También tenemos a nuestra disposición el grupo de Google NHibernate-Hispano, donde podrán echarnos una mano con esta librería.

Para poder utilizarla, voy a comentar una serie de pasos a tener en cuenta para su configuración:

    * Descargar la librería de NHibernate.
    * Crear una clase para el manejo de sesión y configuración de NHibernate.
    * Configuración en el archivo web.config.
    * Creación de nuestros objetos de negocio.
    * Creación de los archivos de mapeo.

DESCARGAR LA LIBRERÍA DE NHIBERNATE

En este post, vamos a trabajar con la versión NHibernate-2.1.0.GA. Para descargar la misma podemos hacerlo a través del siguiente enlace.

Como a día de hoy, nos encontramos en la versión ASP.NET MVC 1.0 y la 2.0 sigue estando en fase de desarrolo, vamos a utilizar la 1.0 para crear el ejemplo. Dicho esto, creamos un proyecto MVC (Si tenemos las dos versiones instaladas, se pueden diferenciar perfectamente ya que MVC 2.0 Preview 1 ó 2 tiene un 2 en el nombre de la plantilla).

En mi caso, voy a llamar a mi proyecto MovieManager. Confirmamos la creación del proyecto y lo primero que nos sugiere es crear un proyecto de Test. Aceptamos la sugerencia para poder utilizala más adelante :D

Llegados a este punto, es necesario añadir una serie de referencias a nuestro proyecto. Descomprimimos el paquete que acabamos de descargar y añadimos las siguientes:

  •   NHibernate.dll
  •  Iesi.Collections.dll
  •  log4net.dll
  •  NHibernate.ByteCode.LinFu.dll

Cómo ya expliqué en un post anterior sobre la estructura de un proyecto ASP.NET MVC, tenemos tres partes bien diferenciadas: Modelo, Controlador, Vista(s). En este post, vamos a comenzar por la parte del modelo donde utilizaremos NHibernate.
En primer lugar, voy a crearme rápidamente una base de datos en SQL Server 2008 Express llamada MovieManager con una sola tabla por el momento.

CREAR UNA CLASE PARA EL MANEJO DE SESIÓN Y CONFIGURACIÓN DE NHIBERNATE

Cuando realizamos consultas a base de datos con NHibernate, es necesario controlar la sesión. A través del objeto session tenemos a nuestra disposición métodos como Save, Update, CreateCriteria, etc. Es muy importante manejar correctamente la sesión aunque espero poder hacerlo en algún post más adelante.

using NHibernate;
using NHibernate.Cfg;

namespace MovieManager.Models
{
    public sealed class Connection
    {
        private static ISessionFactory _sessionFactory;
        private static Configuration _nhConfig;
        private static ISession _session;

        public static ISession Session
        {
            get
            {
                if (_session == null)
                    GetCurrentSession();
                return _session;
            }
        }

        private static void Init()
        {
            _nhConfig = new Configuration();
            _nhConfig.AddAssembly("MovieManager");
            _sessionFactory = _nhConfig.BuildSessionFactory();

        }
        private static ISessionFactory GetSessionFactory()
        {
            if (_sessionFactory == null)
                Init();
            return _sessionFactory;
        }

        private static void GetCurrentSession()
        {
            _session = GetSessionFactory().OpenSession();
        }
    }
}

Actualizado 02/11/2009: Elimino los métodos innecesarios para este post en concreto. Mostraré en otro post diferente cómo sería la manera correcta de manejar la sesión y transacción de NHibernate.

CONFIGURACIÓN DEL ARCHIVO WEB.CONFIG

En el código anterior, teníamos una función Init() que se encargaba de cargar la configuración de NHibernate. Para que esto sea posible es necesario crear una sección en el web.config dónde configuramos todo lo relativo a la base de datos que queremos utilizar.

<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler,NHibernate" requirePermission="false"/>

Una vez declarada la sección, escribimos la misma con los valores necesarios para conectarnos a la base de datos oportuna:

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
    <session-factory>
      <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider, NHibernate</property>
      <property name="connection.connection_string">
        Server=localhostsqlexpress;initial catalog=MovieManager;Integrated Security=true
      </property>
      <property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property>
      <property name='proxyfactory.factory_class'>NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
    </session-factory>
</hibernate-configuration>

Actualizado 02/11/009 : Elimino la propiedad current_session_context_class, ya que por el momento no es necesario establecerla.

Existen varios dialectos soportados para poder utilizar distintos tipos de bases de datos. Para más información sobre ello, os facilito el siguiente enlace.

CREACIÓN DE NUESTROS OBJETOS DE NEGOCIO

La base de datos que tengo en mente almacena información sobre peliculas. Como hasta ahora solamente tenemos una tabla llamada Movies en nuestra base de datos, voy a crear la clase en .NET que representa a dicha tabla.

using System;

namespace MovieManager.Models.Objects
{
    public class Movie
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Genre { get; set; }
        public string Synopsis { get; set; }
        public DateTime Year { get; set; }
    }
}

La clase Movie, no contiene ningún método especial ni atributo que tenga relación con NHibernate. Ese rol, lo tiene el archivo de mapeo que será el que relacione cada propiedad de esta clase con la columna de una tabla específica.

CREACIÓN DE LOS ARCHIVOS DE MAPEO

Para crear el archivo de mapeo necesitamos saber, el nombre de la tabla, el nombre de la clase que representará en .NET a nuestra tabla y los campos que queremos recuperar de base de datos.

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="MovieManager.Models.Objects.Movie,MovieManager" table="Movies" lazy="false">
    <id name="Id" column="Id" unsaved-value="0">
      <generator />
    </id>
    <property name="Name" column="Name" />
    <property name="Genre" column="Genre"/>
    <property name="Synopsis" column="Synopsis" />
    <property name="Year" column="Year" />
  </class>
</hibernate-mapping>

Hay que tener en cuenta un paso muy importante, a la par que olvidadizo :) , y es lo siguiente: El archivo de mapeo debe tener la extensión .hbm.xml y, además, tenemos que asegurarnos de que el Action Build asignado a estos archivos de mapeo sea Embedded Resource ya que de no ser así la configuración de NHibernate no los tendrá en cuenta.

Con estos pasos, nuestra aplicación estaría lista para funcionar con NHibernate, ocupando la parte del Modelo. En el próximo post me centraré en la parte del controlador y vista para poder solicitar información al modelo y que el usuario pueda obtener a través de la vista los resultados recibidos.

¡Saludos!