管理多租户ASP.NET应用程序的NHibernate会话

时间:2010-10-15 02:09:32

标签: asp.net nhibernate session

我有一个现有的多租户ASP.NET应用程序,其中所有用户都针对单个SQL Server数据库进行身份验证。此数据库还包含应用程序中使用的其他几种设置类型数据。身份验证后,每个客户端都使用自己的SQL Server数据库进行数据存储,以实现隔离。基本上所有客户端数据库都是相同的并且驻留在同一服务器上,但也驻留在一个或多个服务器上。

该应用程序目前使用asp.net 2.5框架编写,并使用Microsoft实践企业库进行DAL,我们希望迁移到4.0并实现NHibernate来替换MPEL。

我已经实现了一个已经使用NHibernate和4.0框架的解决方案,所以我熟悉这些概念。事实上,我在这里找到了当前会话管理器的资源。但是该应用程序只有一个数据库,所以也不多。实现基本上是您在这里看到的: http://www.lostechies.com/blogs/nelson_montalvo/archive/2007/03/30/simple-nhibernate-example-part-4-session-management.aspx

我见过的其他解决方案建议使用多个配置条目和/或文件来管理它,但这是不可取的,因为我们可能经常添加新客户端,并且所有连接信息都已在认证数据库中维护。 / p>

有没有人对如何将客户端的连接字符串传递给会话管理器有任何建议?

以下是我当前的会话管理器类,它基于上面提到的文章。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Remoting.Messaging;
using System.Web;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Cache;
using singlepoint.timeclock.domain;

namespace singlepoint.timeclock.repositories
{
    /// <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
    {
        private ISessionFactory idadSessionFactory;
        private ISessionFactory clientSessionFactory;
        private string _client;

        #region Thread-safe, lazy Singleton
        // lazy initialisation, therefore initialised to null
        private static nHibernateSessionManager instance = null;


        /// <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 GetInstance(); }
        }

        public static nHibernateSessionManager GetInstance()
        {
            // lazy init.
            if (instance == null)
                instance = new nHibernateSessionManager();

            return instance;
        } // GetInstance

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

        /// <summary>
        /// Initializes the NHibernate session factory upon instantiation.
        /// </summary>
        private nHibernateSessionManager(string client)
        {
            InitSessionFactory();
            InitClientSessionFactory(client);
        }

        /// <summary>
        /// Assists with ensuring thread-safe, lazy singleton
        /// </summary>
        private class Nested
        {
            static Nested()
            {
            }

            internal static readonly nHibernateSessionManager nHibernatenHibernateSessionManager = new nHibernateSessionManager();
        }

        #endregion

        private void InitSessionFactory()
        {
            var configuration = new Configuration();
            configuration.Configure(System.Configuration.ConfigurationManager.AppSettings["IDAD_HBM"]);
            configuration.AddAssembly(typeof(enterprise).Assembly);
            idadSessionFactory = configuration.BuildSessionFactory();
        }

        private void InitClientSessionFactory(string client)
        {
            var configuration = new Configuration();
            configuration.Configure(System.Configuration.ConfigurationManager.AppSettings["Client_IDAD_HBM"]);
            configuration.SetProperty("connection.connection_string", client);
            configuration.AddAssembly(typeof(enterprise).Assembly);
            clientSessionFactory = configuration.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 = ThreadSession;

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

            GetSession(interceptor);
        }

        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 = ThreadSession;

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

                ThreadSession = session;
            }

            return session;
        }

        public void CloseSession()
        {
            ISession session = ThreadSession;
            ThreadSession = null;

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

        public void BeginTransaction()
        {
            ITransaction transaction = ThreadTransaction;

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

        public void CommitTransaction()
        {
            ITransaction transaction = ThreadTransaction;

            try
            {
                if (transaction != null && !transaction.WasCommitted && !transaction.WasRolledBack)
                {
                    transaction.Commit();
                    ThreadTransaction = null;
                }
            }
            catch (HibernateException ex)
            {
                RollbackTransaction();
                throw ex;
            }
        }

        public void RollbackTransaction()
        {
            ITransaction transaction = ThreadTransaction;

            try
            {
                ThreadTransaction = null;

                if (transaction != null && !transaction.WasCommitted && !transaction.WasRolledBack)
                {
                    transaction.Rollback();
                }
            }
            catch (HibernateException ex)
            {
                throw ex;
            }
            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 ThreadTransaction
        {
            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 ThreadSession
        {
            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 static bool IsInWebContext()
        {
            return HttpContext.Current != null;
        }

        private const string TRANSACTION_KEY = "CONTEXT_TRANSACTION";
        private const string SESSION_KEY = "CONTEXT_SESSION";

        [Obsolete("only until we can fix the session issue globally")]
        internal ISession OpenSession()
        {
            return idadSessionFactory.OpenSession();
        }
    }
}

这是从类似的存储库类调用的:

public string getByName(string name)
{
    return getByName(nHibernateSessionManager.Instance.GetSession(), name);
}

我真正希望能够做到的是:

public string getByName(string name, string clientConnectionString)
{
    return getByName(nHibernateSessionManager.Instance.GetSession(clientConnectionString), name);
}

但我无法修改现有的会话管理器以适应这种情况。

2 个答案:

答案 0 :(得分:2)

您似乎要求为给定会话交换连接。或者更确切地说,这就是你所编写的代码所要求的 - “返回由name参数标识的会话,现在它也应该使用此方法提供的连接字符串。”

这是不可能的。 NHibernate为每个连接构建一个会话(实际上是一个会话工厂),一旦构建了工厂,会话就是不可变的。您无法更改现有会话的连接。

我的印象是,您的应用程序主要涉及作为移动目标的初始连接字符串,但之后您的“真实”会话位于单个数据库上。如果是这样的话,NHibernate可以很容易地做到这一点。如果不是这样的话,那么NHibernate的某些东西并不适合。也许对NHibernate运行的基础有更多了解是有帮助的吗?

我对NHibernate的一个真正的批评是,你对术语有一些神秘的使用,以及它的异常消息众所周知的无益性质。这些事实加上它正在做的事实上实际上机械复杂往往真正模糊,因为有一个相对简单和技术上合理的底层模型。

在这种情况下,如果你考虑一下这个不可变会话的业务很有意义。 NHibernate连接到数据库,但它也维护会话中的对象,以便以后可以将它们持久保存回该数据库。 NHibernate不支持更改每个会话的连接,因为会话中可能已经存在其他对象,如果更改连接,则不再保证其持久性。

现在,您可以做的是为每个数据库创建一个工厂/会话,用于多个数据库并在一个应用程序中访问它们,但对象仍然属于他们自己的会话。您甚至可以使用不同的连接将对象移动到新会话。在这种情况下,您将拥有逻辑上的“复制”方案。 NHibernate支持这一点,但你必须完成大部分工作。这也是有道理的 - 他们真的无法提供稳定的开箱即用功能,你必须自己管理这样的流程。

您还可以构建代码以完全按照您的要求进行操作。但想想那是什么。进行会话,而不是每个数据库,但仅针对此特定存储库的此特定实例。我想这很可能不是你想要的。但这正是您的请求的语义所要做的。另一方面,您现有的类是基于不同的语义构建的,这些语义通常是人们想要的 - “为这个特定的连接定义构建一个会话,即这个数据库。”

真正需要在存储库级别注入连接字符串意味着现在不仅数据库是移动目标,而且在实际表级别,目标也会移动。如果是这种情况,NHibernate可能不是一个好选择。如果不是这种情况,您可能会尝试混合编程范例。 NHiberate强加了一些我称之为“假设”的东西而不是任何真正的“限制”,作为回报,你不必编写一堆代码来让你获得更好的控制,因为你通常不会需要额外的控制。

很抱歉,如果这不再是您问题的直接答案,希望它有所帮助。


原始答案:

当然,由于连接信息在验证数据库中,因此很容易。

1)以“通常”方式配置NHibernate,并将配置指向认证数据库。获取用户的数据库连接,然后关闭该会话和会话工厂。你现在已经完成了。

2)这次在代码而不是配置文件中创建一个新的会话等。

class MyNewSession
 {

     private ISession _session;
     private ISessionFactory _factory;

     public void InitializeSession()
     {

        NHibernate.Cfg.Configuration config = new NHibernate.Cfg.Configuration();
        config.Properties.Clear();

        IDictionary props = new Hashtable();

        // Configure properties as needed, this is pretty minimal standard config here.  
        // Can read in properties from your own xml file or whatever.   

         // Just shown hardcoded here.

        props["proxyfactory.factory_class"] = "NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle";
        props["connection.provider"] = "NHibernate.Connection.DriverConnectionProvider";
        props["dialect"] = "NHibernate.Dialect.MsSql2000Dialect";
        props["connection.driver_class"] = "NHibernate.Driver.SqlClientDriver";
        props["connection.connection_string"] = "<YOUR CONNECTION STRING HERE>";
        props["connection.isolation"] = "ReadCommitted";

        foreach (DictionaryEntry de in props)
        {
            config.Properties.Add(de.Key.ToString(), de.Value.ToString());
        }


        // Everything from here on out is the standard NHibernate calls 
        // you already use.

        // Load mappings etc, etc

        // . . . 


        _factory = config.BuildSessionFactory();
        _session = _factory.OpenSession();
    }

}

答案 1 :(得分:0)

我知道这已经过时但是如果你还没找到解决方案我希望这会有所帮助, 我使用unhaddins创建了一个使用multisessionfactory的解决方案(我根据自己的需要进行了改动)。 基本上多会话工厂为每个数据库创建会话工厂并存储在Application对象中。

取决于客户端,对getfactory的调用(“工厂配置文件中的工厂名称”)返回正确的数据库以进行查询。

您必须更改管理模块以支持此存储库和所有存储库以支持管理更改。一开始这可能是不切实际的,但无论如何你必须改变它们。来自存储库的调用可能是这样的:

public string getByName(string name)
{
    return getByName(nHibernateSessionManager.SessionFactoryManager.GetFactory(Session["session variable that holds client session factory name that was set on login"]).GetCurrentSession(), name);
}

或(在sessionmanager中创建一个返回给定工厂会话的方法)你的代码可能是这样的

public string getByName(string name)
{
    return getByName(nHibernateSessionManager.GetSession(Session["session variable that holds client session factory name that was set on login"]), name);
}