Gestion des sessions NHibernate

La gestion des sessions est un aspect crucial pour tirer avantage de l’utilisation de NHibernate. Le problème vient du fait que la construction du SessionFactory qui gère les transactions et sessions de NHibernate est assez coûteuse. Il existe plusieurs stratégies pour gérer ce problème avec ASP.Net. Dave Byron propose une solution assez simple via un pattern de Unit of Work. Toutefois, cette méthode nécessite également des connaissances des patterns de Depency Injection et de Inversion of Control.

C’est pourquoi celle que je présente ici est exactement celle proposée par Billy McCafferty dans son article NHibernate Best Practices with ASP.Net. La méthode utilise un pattern de Singleton dont l’implantation en C# est présentée sur cette page. Un avantage de cette méthode est qu’elle fonctionnera que vous utilisiez ASP.Net ou bien les WinForms.

using System.Runtime.Remoting.Messaging;
using System.Web;
using NHibernate;
using NHibernate.Cache;
using NHibernate.Cfg;

namespace NHibernateHelper
{
    /// <summary>
    /// Handles creation and management of sessions and transactions.  It is a singleton because 
    /// building the initial session factory is very expensive. Inspiration for this class came 
    /// from Chapter 8 of Hibernate in Action by Bauer and King.  Although it is a sealed singleton
    /// you can use TypeMock (http://www.typemock.com) for more flexible testing.
    /// </summary>
    public sealed class NHibernateSessionManager
    {
        #region Thread-safe, lazy Singleton

        /// <summary>
        /// This is a thread-safe, lazy singleton.  See http://www.yoda.arachsys.com/csharp/singleton.html
        /// for more details about its implementation.
        /// </summary>
        public static NHibernateSessionManager Instance {
            get {
                return Nested.NHibernateSessionManager;
            }
        }

        /// <summary>
        /// Initializes the NHibernate session factory upon instantiation.
        /// </summary>
        private NHibernateSessionManager() {
            InitSessionFactory();
        }

        /// <summary>
        /// Assists with ensuring thread-safe, lazy singleton
        /// </summary>
        private class Nested
        {
            static Nested() { }
            internal static readonly NHibernateSessionManager NHibernateSessionManager = 
                new NHibernateSessionManager();
        }

        #endregion

        private void InitSessionFactory() {
            sessionFactory = new Configuration().Configure().BuildSessionFactory();
        }

        /// <summary>
        /// Allows you to register an interceptor on a new session.  This may not be called if there is already
        /// an open session attached to the HttpContext.  If you have an interceptor to be used, modify
        /// the HttpModule to call this before calling BeginTransaction().
        /// </summary>
        public void RegisterInterceptor(IInterceptor interceptor) {
            ISession session = ContextSession;

            if (session != null && session.IsOpen) {
                throw new CacheException("You cannot register an interceptor once a session has already been opened");
            }

            GetSession(interceptor);
        }

        /// <summary>
        /// Gets a session
        /// </summary>
        /// <returns></returns>
        public ISession GetSession() {
            return GetSession(null);
        }

        /// <summary>
        /// Gets a session with or without an interceptor.  This method is not called directly; instead,
        /// it gets invoked from other public methods.
        /// </summary>
        private ISession GetSession(IInterceptor interceptor) {
            ISession session = ContextSession;

            if (session == null) {
                if (interceptor != null) {
                    session = sessionFactory.OpenSession(interceptor);
                }
                else {
                    session = sessionFactory.OpenSession();
                }

                ContextSession = session;
            }

            return session;
        }

        /// <summary>
        /// Flushes anything left in the session and closes the connection.
        /// </summary>
        public void CloseSession() {
            ISession session = ContextSession;

            if (session != null && session.IsOpen) {
                session.Flush();
                session.Close();
            }

            ContextSession = null;
        }

        /// <summary>
        /// Begins a transaction
        /// </summary>
        public void BeginTransaction() {
            ITransaction transaction = ContextTransaction;

            if (transaction == null) {
                transaction = GetSession().BeginTransaction();
                ContextTransaction = transaction;
            }
        }

        /// <summary>
        /// Commits a transaction
        /// </summary>
        public void CommitTransaction() {
            ITransaction transaction = ContextTransaction;

            try {
                if (HasOpenTransaction()) {
                    transaction.Commit();
                    ContextTransaction = null;
                }
            }
            catch (HibernateException) {
                RollbackTransaction();
                throw;
            }
        }

        /// <summary>
        /// Checks if the transaction is opened
        /// </summary>
        /// <returns></returns>
        public bool HasOpenTransaction() {
            ITransaction transaction = ContextTransaction;

            return transaction != null && !transaction.WasCommitted && !transaction.WasRolledBack;
        }

        /// <summary>
        /// Rollback the transaction
        /// </summary>
        public void RollbackTransaction() {
            ITransaction transaction = ContextTransaction;

            try {
                if (HasOpenTransaction()) {
                    transaction.Rollback();
                }

                ContextTransaction = null;
            }
            finally {
                CloseSession();
            }
        }

        /// <summary>
        /// If within a web context, this uses <see cref="HttpContext" /> instead of the WinForms 
        /// specific <see cref="CallContext" />.  Discussion concerning this found at 
        /// http://forum.springframework.net/showthread.php?t=572.
        /// </summary>
        private ITransaction ContextTransaction {
            get {
                if (IsInWebContext()) {
                    return (ITransaction)HttpContext.Current.Items[TRANSACTION_KEY];
                }
                else {
                    return (ITransaction)CallContext.GetData(TRANSACTION_KEY);
                }
            }
            set {
                if (IsInWebContext()) {
                    HttpContext.Current.Items[TRANSACTION_KEY] = value;
                }
                else {
                    CallContext.SetData(TRANSACTION_KEY, value);
                }
            }
        }

        /// <summary>
        /// If within a web context, this uses <see cref="HttpContext" /> instead of the WinForms 
        /// specific <see cref="CallContext" />.  Discussion concerning this found at 
        /// http://forum.springframework.net/showthread.php?t=572.
        /// </summary>
        private ISession ContextSession {
            get {
                if (IsInWebContext()) {
                    return (ISession)HttpContext.Current.Items[SESSION_KEY];
                }
                else {
                    return (ISession)CallContext.GetData(SESSION_KEY); 
                }
            }
            set {
                if (IsInWebContext()) {
                    HttpContext.Current.Items[SESSION_KEY] = value;
                }
                else {
                    CallContext.SetData(SESSION_KEY, value);
                }
            }
        }

        private bool IsInWebContext() {
            return HttpContext.Current != null;
        }

        private const string TRANSACTION_KEY = "nhibernate.context.transaction.key";
        private const string SESSION_KEY = "nhibernate.context.session.key";
        private ISessionFactory sessionFactory;
    }
}

Un petite note: peu importe la méthode utilisée, si vous êtes dans un contexte Web, il faut généralement s’assurer que la méthode est Thread-Safe.

Une question demeure toutefois, quand doit-on créer les sessions et transactions et quand doit-on les fermer? La documentation suggère de mettre en oeuvre le pattern Open-In-Session-View afin de mettre à profit les possiblités de lazy-loading de NHibernate. Billy McCafferty mentionnait dans son article:

If you want to leverage NHibernate’s lazy-loading (which you most certainly will), then the Open-Session-in-View pattern is the way to go. (“Session” in this context is the NHibernate ISession…not the ASP.NET Session object.) Essentially, this pattern suggests that one NHibernate session be opened per HTTP request.

Encore une fois, l’approche proposée est celle que je présente. Le pattern signifie qu’une session NHibernate doit être ouverte par requête HTTP. En asp.net, le plus simple pour accomplir cette tâche à mon avis est en effet de créer un IHttpModule. On crée la session au début de la requête et on la ferme à la fin. S’il y a des changements à effectués, ils seront engagés avant la fermeture de la session. S’il y a des erreurs, la transaction sera annulée et la session sera quand même fermée. Voici le code du module qui se veut assez simple.

using System;
using System.Web;

namespace NHibernateHelper.Modules
{
    /// <summary>
    /// Implements the Open-Session-In-View pattern using <see cref="NHibernateSessionManager" />.
    /// Assumes that each HTTP request is given a single transaction for the entire page-lifecycle.
    /// Inspiration for this class came from Ed Courtenay at 
    /// http://sourceforge.net/forum/message.php?msg_id=2847509.
    /// </summary>
    public class NHibernateSessionModule : IHttpModule
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        public void Init(HttpApplication context) {
            context.BeginRequest += new EventHandler(BeginTransaction);
            context.EndRequest += new EventHandler(CommitAndCloseSession);
        }

        /// <summary>
        /// Opens a session within a transaction at the beginning of the HTTP request.
        /// This doesn't actually open a connection to the database until needed.
        /// </summary>
        private void BeginTransaction(object sender, EventArgs e) {
            NHibernateSessionManager.Instance.BeginTransaction();
        }

        /// <summary>
        /// Commits and closes the NHibernate session provided by the supplied <see cref="NHibernateSessionManager"/>.
        /// Assumes a transaction was begun at the beginning of the request; but a transaction or session does
        /// not *have* to be opened for this to operate successfully.
        /// </summary>
        private void CommitAndCloseSession(object sender, EventArgs e) {
            try {
                NHibernateSessionManager.Instance.CommitTransaction();
            }
            finally {
                NHibernateSessionManager.Instance.CloseSession();
            }
        }

        /// <summary>
        /// IDisposable implementation
        /// </summary>
        public void Dispose() { }
    }
}

Voilà, il ne vous reste plus qu’à configurer le module http dans votre web.config et vous serez en mesure de gérer vos sessions NHibernate sans trop de problèmes.

Finalement, un simple exemple pour exécuter une requête via notre gestionnaire de session:

ISession session = NHibernateSessionManager.Instance.GetSession(); 
return session.Get<T>(id);
Publié le 23 novembre 2008

Développeur, sportif et technologue.
Dominic Bégin on Twitter