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

2023-11-26

我正在构建一个多租户 Web 应用程序,出于安全考虑,我们需要为每个租户拥有一个数据库实例。所以我有一个用于身份验证的 MainDB 和许多用于应用程序数据的 ClientDB。

我正在使用 Asp.net MVC 与 Ninject 和 Fluent nHibernate。我已经在应用程序开始时在 Ninject 模块中使用 Ninject 和 Fluent nHibernate 设置了 SessionFactory/Session/Repositories。我的会话是 PerRequestScope,存储库也是如此。

我的问题是现在我需要为每个租户实例化一个 SessionFactory (SingletonScope) 实例,只要其中一个租户连接到应用程序,并为每个 Web 请求创建一个新会话和必要的存储库。我对如何做到这一点感到困惑,并且需要一个具体的例子。

情况是这样的。

申请开始:TenantX 的用户输入他的登录信息。 MainDB 的 SessionFactory 被创建并打开到 MainDB 的会话以对用户进行身份验证。然后应用程序创建身份验证 cookie。

租户访问应用程序:租户名称 + ConnectionString 是从 MainDB 中提取的,Ninject 必须为该租户构造一个特定于租户的 SessionFactory (SingletonScope)。 Web 请求的其余部分,所有需要存储库的控制器都将根据该租户的 SessionFactory 注入特定于租户的会话/存储库。

如何使用 Ninject 设置该动态?当我有多个数据库时,我最初使用命名实例,但现在数据库是特定于租户的,我迷失了......


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

虽然可以将连接字符串传递给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-per-tenant 的工作实现。

The 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。

When CreateSession()被称为我们得到一个ISessionFactory实例。这将是默认会话工厂(如果当前租户为空)或特定于租户的会话工厂。定位租户特定会话工厂的任务由GetSessionFactory method.

最后我们打电话OpenSession on the ISessionFactory我们获得的实例。

请注意,当我们创建会话工厂时,我们设置 SessionFactory 名称(用于调试/分析目的)和缓存区域前缀(出于上述原因)。

我们的 IoC 工具(在我的例子中为 StructureMap)将所有内容连接起来:

    x.For<ISessionSource>().Singleton().Use<NHibernateSessionSource>();
    x.For<ISession>().HttpContextScoped().Use(ctx => 
        ctx.GetInstance<ISessionSource>().CreateSession());
    x.For<ITenantAccessor>().Use<DefaultTenantAccessor>();

这里 NHibernate Session Source 的作用域是单例和每个请求的 Session。

希望这可以帮助。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 Fluent nHibernate 和 Ninject 实现多租户。每个租户一个数据库 的相关文章

随机推荐

  • 在 PHP 中迭代复杂的关联数组

    有没有一种简单的方法可以在 PHP 中迭代此结构的关联数组 数组 searches有一个编号索引 包含 4 到 5 个关联部分 所以我不仅需要迭代 searches 0 通过 searches n 但是也 searches 0 part0
  • 获取有关 MATLAB com.mathworks 内部结构的帮助

    可以访问 MATLAB 的内部 java 代码位 以编程方式更改 MATLAB 本身 例如 您可以使用以下命令以编程方式在编辑器中打开文档 editorServices com mathworks mlservices MLEditorSe
  • UITableView自定义滚动条

    如何为 UITableView 创建自定义滚动条 我想删除跟踪开始时弹出并在跟踪结束时消失的默认选项 相反 我想要一个类似于计算机程序中的程序 a 它位于屏幕的右侧并且永久可见 b 手动滚动栏会将 UITableView 滚动到适当的位置
  • 禁用特定 GDI 设备上下文的抗锯齿功能

    我正在使用第三方库将图像渲染到 GDI DC 并且我需要确保渲染任何文本时都不会进行任何平滑 抗锯齿 以便我可以将图像转换为具有索引颜色的预定义调色板 我用于渲染的第三方库不支持此功能 并且仅根据当前 Windows 设置的字体渲染来渲染文
  • 如何在 Haskell 中使用策略编写并行归约?

    在高性能计算中 总和 乘积等通常使用 并行归约 来计算 该方法需要n元素并在 O logn 时间 给定足够的并行性 在 Haskell 中 我们通常使用fold对于这种计算 但评估时间始终与列表的长度呈线性关系 Data Parallel
  • 固定宽度整数类型是否保证是标准内置类型的 typedef?

    类型是否来自
  • 如何在写入后清除 PrintWriter 的内容

    晚上好 我想知道如何清除写入PrintWriter的数据 即打印后是否可以从PrintWriter中删除数据 在此 servlet 中 我将一些文本打印到响应中 并在 表示的行处 我想删除所有以前打印的数据并打印新内容 protected
  • Kivy 布局高度适应子部件的高度

    我想创建一个布局 其中有类似于 BoxLayout 的内容 以便我能够在布局中创建 行 并且在每个 行 中我想使用另一个 BoxLayout 中的某些内容来创建 列 列不需要均匀分布 例如 我想创建一个 BoxLayout 其中一列带有方形
  • 如何在codeigniter中的hmvc中的另一个模块中加载模型?

    我想在我的项目中使用模块化扩展 HMVC 如下所示 modules module01 models models01 php controllers controller01 php views views01 php module02 m
  • 我应该切换到Python吗? [关闭]

    很难说出这里问的是什么 这个问题模棱两可 含糊不清 不完整 过于宽泛或言辞激烈 无法以目前的形式合理回答 如需帮助澄清此问题以便重新打开 访问帮助中心 我最近一直在考虑转向Python编程语言 目前 Matlab 是我所在部门用于快速开发和
  • 在 WPF C# TreeView 中获取子节点的父节点

    我知道使用 WPF 进行 C 编程与传统的 C 程序不同 因此大多数在线材料都没有说明我需要什么 我的 WPF 窗口中有一个 TreeView 控件 其中有父节点和子节点 我想将它们存储在 Node 类型的列表中 id name paren
  • 如果命令绑定解析为 null,为什么按钮会启用?

    好的 XAML 非常简单 使用 MVVM 绑定到ICommand SomeCommand get 视图模型上的属性
  • 我的程序如何从 ASCII 切换到 Unicode?

    我想用 C 编写一个可以在 Unix 和 Windows 上运行的程序 该程序应该能够使用 Unicode 和非 Unicode 环境 它的行为应该仅取决于环境设置 我想要的一个很好的功能是操作从目录中读取的文件名 这些可以是 unicod
  • 如何在C++中读取格式化数据?

    我已将数据格式化如下 Words 5 AnotherWord 4 SomeWord 6 它在一个文本文件中 我使用 ifstream 来读取它 但如何分离数字和单词 该单词仅由字母组成 单词和数字之间会有一定的空格或制表符 不确定有多少 假
  • 为什么 CDATA 在脚本标签下被注释掉?

    我正在读这个question我有一个相关的问题 这家伙here说 它用在脚本标签中以避免解析 already CDATA Question 1 如果脚本是already CDATA 为什么它 在脚本标签下 仍然呈现为 CDATA Quest
  • Sql Server 中的“IN”子句限制

    有谁知道 IN 子句的表达式列表 用于测试匹配 中可以拥有的值的数量限制是多少 是的 有限制 但是微软仅指定其位于 数千 在 IN 子句中的括号内显式包含大量值 数千个用逗号分隔的值 可能会消耗资源并返回错误 8623 或 8632 要解决
  • 我在哪里可以获得线程安全的 CollectionView?

    在后台线程上更新业务对象集合时 我收到以下错误消息 这种类型的 CollectionView 不支持从与 Dispatcher 线程不同的线程更改其 SourceCollection 好吧 这是有道理的 但它也引出了一个问题 什么版本的 C
  • Java 中原始整数类型的行为不一致

    有人可以向我解释一下 就像我五岁一样 为什么我在 Java 中表示整数的四种基本类型中的两种会得到不同的行为 AFAIK 所有四个都是有符号的 并且它们都使用最高有效位作为符号位 那么为什么 byte 和 Short 表现正常 而 int
  • 显示进度条,显示表单提交的进度

    这个问题还没有完全解答 欢迎大家踊跃留言 我正在尝试显示一个简单的progress bar提交大表格时 该表单包含十几个字段 以及一些文件上传字段 用户可以在其中选择图片 然后 当他点击Create按钮 提交带有数据和图片的表单 并在数据库
  • 使用 Fluent nHibernate 和 Ninject 实现多租户。每个租户一个数据库

    我正在构建一个多租户 Web 应用程序 出于安全考虑 我们需要为每个租户拥有一个数据库实例 所以我有一个用于身份验证的 MainDB 和许多用于应用程序数据的 ClientDB 我正在使用 Asp net MVC 与 Ninject 和 F