使用Fluent nHibernate和Ninject实现多租户,每个租户一个数据库。

7
我正在构建一个多租户Web应用程序,出于安全考虑,我们需要每个租户拥有一个数据库实例。因此,我有一个MainDB用于身份验证和许多ClientDB用于应用程序数据。
我使用Asp.net MVC、Ninject和Fluent nHibernate。我已经在应用程序开始时使用Ninject和Fluent nHibernate设置了我的SessionFactory/Session/Repositories。我的sessions和repositories都是PerRequestScope的。
现在我的问题是当其中一个租户连接到应用程序时,我需要为我的每个租户实例化一个SessionFactory(SingletonScope),并为每个web请求创建一个新的session和必要的repositories。我对如何做这件事感到困惑,并需要一个具体的示例。
以下是情况:
应用程序启动:TenantX的用户输入其登录信息。MainDB的SessionFactory被创建并打开一个会话以对用户进行身份验证。然后应用程序创建了auth cookie。
租户访问应用程序:从MainDB中提取租户名称+连接字符串,Ninject必须为该租户构建特定的SessionFactory(SingletonScope)。在web请求的其余部分中,所有需要repository的控制器都将基于该租户的SessionFactory注入一个特定于租户的session/repository。
我该如何使用Ninject设置这种动态性?当我有多个数据库时,我最初使用Named实例,但现在数据库是特定于租户的,我迷失了……

用户登录后,您如何使其能够更改密码?我假设他们的密码在“MainDB”而不是“TenantDB”中,对吗? - Richard B
2个回答

11

经过进一步的研究,我可以给你一个更好的答案。

虽然可以将连接字符串传递给ISession.OpenSession,但更好的方法是创建自定义ConnectionProvider。 最简单的方法是从DriverConnectionProvider派生并重写ConnectionString属性:

public class TenantConnectionProvider : DriverConnectionProvider
{
    protected override string ConnectionString
    {
        get
        {
            // load the tenant connection string
            return "";
        }
    }

    public override void Configure(IDictionary<string, string> settings)
    {
        ConfigureDriver(settings);
    }
}

使用FluentNHibernate设置提供程序的方法如下:

var config = Fluently.Configure()
    .Database(
        MsSqlConfiguration.MsSql2008
            .Provider<TenantConnectionProvider>()
    )

每次打开会话时,ConnectionProvider都会被评估,允许您在应用程序中连接到特定于租户的数据库。

上述方法的一个问题是SessionFactory是共享的。如果您只使用第一级缓存(因为这与会话相关),那么这不是真正的问题,但如果您决定启用第二级缓存(与SessionFactory相关联),则会出现问题。

因此,建议的方法是针对每个租户具有一个SessionFactory(这适用于基于租户架构和基于租户数据库的策略)。

另一个经常被忽视的问题是,尽管第二级缓存与SessionFactory相关联,但在某些情况下,缓存空间本身是共享的(参考)。可以通过设置提供程序的“regionName”属性来解决此问题。

以下是根据您的要求基于每个租户的SessionFactory的工作实现。

Tenant类包含我们需要为租户设置NHibernate的信息:

public class Tenant : IEquatable<Tenant>
{
    public string Name { get; set; }
    public string ConnectionString { get; set; }

    public bool Equals(Tenant other)
    {
        if (other == null)
            return false;

        return other.Name.Equals(Name) && other.ConnectionString.Equals(ConnectionString);
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Tenant);
    }

    public override int GetHashCode()
    {
        return string.Concat(Name, ConnectionString).GetHashCode();
    }
}

由于我们将存储一个Dictionary<Tenant, ISessionFactory>,因此我们实现了IEquatable接口,以便我们可以评估租户键。

获取当前租户的过程如下所示:

public interface ITenantAccessor
{
    Tenant GetCurrentTenant();
}

public class DefaultTenantAccessor : ITenantAccessor
{
    public Tenant GetCurrentTenant()
    {
        // your implementation here

        return null;
    }
}

最后是管理会话的NHibernateSessionSource

public interface ISessionSource
{
    ISession CreateSession();
}

public class NHibernateSessionSource : ISessionSource
{
    private Dictionary<Tenant, ISessionFactory> sessionFactories = 
        new Dictionary<Tenant, ISessionFactory>();

    private static readonly object factorySyncRoot = new object();

    private string defaultConnectionString = 
        @"Server=(local)\sqlexpress;Database=NHibernateMultiTenancy;integrated security=true;";

    private readonly ISessionFactory defaultSessionFactory;
    private readonly ITenantAccessor tenantAccessor;

    public NHibernateSessionSource(ITenantAccessor tenantAccessor)
    {
        if (tenantAccessor == null)
            throw new ArgumentNullException("tenantAccessor");

        this.tenantAccessor = tenantAccessor;

        lock (factorySyncRoot)
        {
            if (defaultSessionFactory != null) return;

            var configuration = AssembleConfiguration("default", defaultConnectionString);
            defaultSessionFactory = configuration.BuildSessionFactory();
        }
    }

    private Configuration AssembleConfiguration(string name, string connectionString)
    {
        return Fluently.Configure()
            .Database(
                MsSqlConfiguration.MsSql2008.ConnectionString(connectionString)
            )
            .Mappings(cfg =>
            {
                cfg.FluentMappings.AddFromAssemblyOf<NHibernateSessionSource>();
            })
            .Cache(c =>
                c.UseSecondLevelCache()
                .ProviderClass<HashtableCacheProvider>()
                .RegionPrefix(name)
            )
            .ExposeConfiguration(
                c => c.SetProperty(NHibernate.Cfg.Environment.SessionFactoryName, name)
            )
            .BuildConfiguration();
    }

    private ISessionFactory GetSessionFactory(Tenant currentTenant)
    {
        ISessionFactory tenantSessionFactory;

        sessionFactories.TryGetValue(currentTenant, out tenantSessionFactory);

        if (tenantSessionFactory == null)
        {
            var configuration = AssembleConfiguration(currentTenant.Name, currentTenant.ConnectionString);
            tenantSessionFactory = configuration.BuildSessionFactory();

            lock (factorySyncRoot)
            {
                sessionFactories.Add(currentTenant, tenantSessionFactory);
            }
        }

        return tenantSessionFactory;
    }

    public ISession CreateSession()
    {
        var tenant = tenantAccessor.GetCurrentTenant();

        if (tenant == null)
        {
            return defaultSessionFactory.OpenSession();
        }

        return GetSessionFactory(tenant).OpenSession();
    }
}
创建一个NHibernateSessionSource实例时,我们将默认的SessionFactory设置为“默认”数据库。
当调用CreateSession()方法时,我们获取一个ISessionFactory实例。这将是默认会话工厂(如果当前租户为null)或特定于租户的会话工厂。查找特定于租户的会话工厂的任务由GetSessionFactory方法执行。
最后,我们在获得的ISessionFactory实例上调用OpenSession方法。
请注意,当我们创建一个SessionFactory时,我们设置了SessionFactory名称(用于调试/分析目的)和缓存区域前缀(出于上述原因)。
我们的IoC工具(在我的情况下为StructureMap)将所有东西连接起来。
    x.For<ISessionSource>().Singleton().Use<NHibernateSessionSource>();
    x.For<ISession>().HttpContextScoped().Use(ctx => 
        ctx.GetInstance<ISessionSource>().CreateSession());
    x.For<ITenantAccessor>().Use<DefaultTenantAccessor>();

这里将NHibernateSessionSource作为单例进行作用域控制,每个请求使用一个ISession。

希望这可以帮到你。


在这种情况下,请查看我发布的第二个链接。 - Ben Foster
虽然这可以解决nHibernate缓存问题,但我的要求是每个租户一个数据库,因此每个租户一个架构的单个数据库不起作用。 - Nick
嗨,理查德。好问题。本,我一直在密切关注这个帖子,因为我和理查德的情况完全相同。好东西! - Todd
Ben,你提到要向Tenant类添加一个属性。像'MasterConnectionString'这样的属性是什么类型?如何指定要使用主连接字符串的会话?在'public Tenant GetCurrentTenant()'中将使用Master数据库会话来查找租户信息。一旦查找完毕,就会将租户缓存,以免每个请求都要进行查找。你有什么想法? - Todd
Ben,感谢你的回答,它是最相关的之一。但是,在// your implementation here这一行中仍然存在一个缺点,返回租户需要调用defaultDatabase才能找到它。如果您自己尝试一下,您会注意到它变成了“循环引用”。 - Cristian E.
显示剩余4条评论

0
如果所有的数据库都在同一台机器上,也许可以使用类映射的模式属性来在预租户基础上设置数据库。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接