Multitenancy с беглым nHibernate и Ninject. Одна база данных на одного арендатора - PullRequest
7 голосов
/ 26 января 2012

Я создаю мультитенантное веб-приложение, в котором из соображений безопасности нам нужен один экземпляр базы данных на каждого арендатора.Поэтому у меня есть MainDB для аутентификации и много ClientDB для данных приложения.

Я использую Asp.net MVC с Ninject и Fluent nHibernate.Я уже настроил свои SessionFactory / Session / Repositories, используя Ninject и Fluent nHibernate в модуле Ninject при запуске приложения.Мои сеансы PerRequestScope, как и репозитории.

Теперь моя проблема заключается в том, что мне нужно создавать экземпляр SessionFactory (SingletonScope) для каждого из моих арендаторов, когда один из них подключается к приложению, и создавать новый сеанс и необходимые репозитории.за каждый веб-запрос.Я озадачен тем, как это сделать, и мне нужен конкретный пример.

Вот ситуация.

Приложение запускается : пользовательof TenantX вводит свою регистрационную информацию.SessionFactory из MainDB создается и открывает сеанс для MainDB для аутентификации пользователя.Затем приложение создает cookie-файл авторизации.

Клиент получает доступ к приложению : имя арендатора + ConnectionString извлекаются из MainDB, и Ninject должен создать специфическую для арендатора SessionFactory (SingletonScope) для этого арендатора.В остальной части веб-запроса все контроллеры, которым требуется репозиторий, будут внедрены в специфический для Арендатора сеанс / репозиторий на основе SessionFactory этого арендатора.

Как настроить эту динамику с помощью Ninject?Первоначально я использовал именованный экземпляр, когда у меня было несколько баз данных, но теперь, когда базы данных являются специфичными для арендатора, я потерян ...

Ответы [ 2 ]

11 голосов
/ 26 января 2012

После дальнейших исследований я могу дать вам лучший ответ.

Хотя можно передать строку соединения в 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-per-tenant (это будет применяться к стратегиям схемы-на-арендатора и базы данных на арендатора).

Другая проблема, которая часто упускается из виду, заключается в том, что, хотя кэш второго уровня привязан кSessionFactory, в некоторых случаях само пространство кеша используется совместно ( ссылка ).Эту проблему можно решить, установив свойство «regionName» поставщика.

Ниже приведена рабочая реализация SessionFactory-per-tenant, основанная на ваших требованиях.

Класс 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.Это будет либо фабрика сеансов по умолчанию (если текущий арендатор равен нулю), либо фабрика сеансов, специфичная для арендатора.Задача поиска фабрики сеансов для конкретного арендатора выполняется методом GetSessionFactory.

Наконец, мы вызываем OpenSession для полученного нами экземпляра 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>();

Здесь NHibernateSessionSource определяется как одиночный и ISession для запроса.

Надеюсь, это поможет.

0 голосов
/ 26 января 2012

Если все базы данных находятся на одном и том же компьютере, возможно, можно использовать свойство схемы сопоставлений классов для установки базы данных на основе предварительного арендатора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...