Abp vNext:多租户如何切换数据库

发布时间 2024-01-04 18:12:27作者: easy5

资料

Abp vNext:多租户:https://docs.abp.io/en/abp/latest/Multi-Tenancy

多租户的数据库架构

Abp vNext:多租户的数据库

ABP Framework supports all the following approaches to store the tenant data in the database;

Single Database: All tenants are stored in a single database.
Database per Tenant: Every tenant has a separate, dedicated database to store the data related to that tenant.
Hybrid: Some tenants share a single databases while some tenants may have their own databases.

多租户如何切换数据库

获取数据上下文接口 IDbContextProvider

using System;
using System.Threading.Tasks;

namespace Volo.Abp.EntityFrameworkCore;

public interface IDbContextProvider<TDbContext>
    where TDbContext : IEfCoreDbContext
{
    [Obsolete("Use GetDbContextAsync method.")]
    TDbContext GetDbContext();

    Task<TDbContext> GetDbContextAsync();
}

该接口实现类:UnitOfWorkDbContextProvider

public class UnitOfWorkDbContextProvider<TDbContext> : IDbContextProvider<TDbContext>
    where TDbContext : IEfCoreDbContext
{
  ......

    public virtual async Task<TDbContext> GetDbContextAsync()
    {
        var unitOfWork = UnitOfWorkManager.Current;
        if (unitOfWork == null)
        {
            throw new AbpException("A DbContext can only be created inside a unit of work!");
        }

        var targetDbContextType = EfCoreDbContextTypeProvider.GetDbContextType(typeof(TDbContext));
        var connectionStringName = ConnectionStringNameAttribute.GetConnStringName(targetDbContextType);
        var connectionString = await ResolveConnectionStringAsync(connectionStringName);

        var dbContextKey = $"{targetDbContextType.FullName}_{connectionString}";

        var databaseApi = unitOfWork.FindDatabaseApi(dbContextKey);

        if (databaseApi == null)
        {
            databaseApi = new EfCoreDatabaseApi(
                await CreateDbContextAsync(unitOfWork, connectionStringName, connectionString)
            );

            unitOfWork.AddDatabaseApi(dbContextKey, databaseApi);
        }

        return (TDbContext)((EfCoreDatabaseApi)databaseApi).DbContext;
    }
  ......
}

获取租户数据库链接字符串

关键代码:

  protected readonly IConnectionStringResolver ConnectionStringResolver;

   var connectionString = await ResolveConnectionStringAsync(connectionStringName);

    protected virtual async Task<string> ResolveConnectionStringAsync(string connectionStringName)
    {
        // Multi-tenancy unaware contexts should always use the host connection string
        if (typeof(TDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
        {
            using (CurrentTenant.Change(null))
            {
                return await ConnectionStringResolver.ResolveAsync(connectionStringName);
            }
        }

        return await ConnectionStringResolver.ResolveAsync(connectionStringName);
    }

其中 ConnectionStringResolver.ResolveAsync() 将调用实现类 MultiTenantConnectionStringResolverResolveAsync() 方法,如下代码所示:

namespace Volo.Abp.MultiTenancy;

[Dependency(ReplaceServices = true)]
public class MultiTenantConnectionStringResolver : DefaultConnectionStringResolver
{
       public override async Task<string> ResolveAsync(string? connectionStringName = null)
    {
        if (_currentTenant.Id == null)
        {
            //No current tenant, fallback to default logic
            return await base.ResolveAsync(connectionStringName);
        }

        var tenant = await FindTenantConfigurationAsync(_currentTenant.Id.Value);

        if (tenant == null || tenant.ConnectionStrings.IsNullOrEmpty())
        {
            //Tenant has not defined any connection string, fallback to default logic
            return await base.ResolveAsync(connectionStringName);
        }

        var tenantDefaultConnectionString = tenant.ConnectionStrings?.Default;

        //Requesting default connection string...
        if (connectionStringName == null ||
            connectionStringName == ConnectionStrings.DefaultConnectionStringName)
        {
            //Return tenant's default or global default
            return !tenantDefaultConnectionString.IsNullOrWhiteSpace()
                ? tenantDefaultConnectionString!
                : Options.ConnectionStrings.Default!;
        }

        //Requesting specific connection string...
        var connString = tenant.ConnectionStrings?.GetOrDefault(connectionStringName);
        if (!connString.IsNullOrWhiteSpace())
        {
            //Found for the tenant
            return connString!;
        }

        //Fallback to the mapped database for the specific connection string
        var database = Options.Databases.GetMappedDatabaseOrNull(connectionStringName);
        if (database != null && database.IsUsedByTenants)
        {
            connString = tenant.ConnectionStrings?.GetOrDefault(database.DatabaseName);
            if (!connString.IsNullOrWhiteSpace())
            {
                //Found for the tenant
                return connString!;
            }
        }

        //Fallback to tenant's default connection string if available
        if (!tenantDefaultConnectionString.IsNullOrWhiteSpace())
        {
            return tenantDefaultConnectionString!;
        }

        return await base.ResolveAsync(connectionStringName);
    }
}

这样就获得了多租户的数据库链接字符串;

获取数据库上下文

回到 该接口实现类:UnitOfWorkDbContextProviderGetDbContextAsync() 方法:

关键代码:

        var connectionString = await ResolveConnectionStringAsync(connectionStringName);
        var databaseApi = unitOfWork.FindDatabaseApi(dbContextKey);

        if (databaseApi == null)
        {
            databaseApi = new EfCoreDatabaseApi(
                await CreateDbContextAsync(unitOfWork, connectionStringName, connectionString)
            );

            unitOfWork.AddDatabaseApi(dbContextKey, databaseApi);
        }

把上一步获取到的租户链接字符串 connectionString 传入 EfCoreDatabaseApi 构造函数的 CreateDbContextAsync() 方法,该方法内容如下:

namespace Volo.Abp.Uow.EntityFrameworkCore;

public class UnitOfWorkDbContextProvider<TDbContext> : IDbContextProvider<TDbContext>
    where TDbContext : IEfCoreDbContext
{
   ......

   protected virtual async Task<TDbContext> CreateDbContextAsync(IUnitOfWork unitOfWork, string connectionStringName, string connectionString)
   {
       var creationContext = new DbContextCreationContext(connectionStringName, connectionString);
       using (DbContextCreationContext.Use(creationContext))
       {
           var dbContext = await CreateDbContextAsync(unitOfWork);

           if (dbContext is IAbpEfCoreDbContext abpEfCoreDbContext)
           {
               abpEfCoreDbContext.Initialize(
                   new AbpEfCoreDbContextInitializationContext(
                       unitOfWork
                   )
               );
           }

           return dbContext;
       }
   }

    protected virtual async Task<TDbContext> CreateDbContextAsync(IUnitOfWork unitOfWork)
    {
        return unitOfWork.Options.IsTransactional
            ? await CreateDbContextWithTransactionAsync(unitOfWork)
            : unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
    }

最终是使用

var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();

获取到数据库上下文:TDbContext : IEfCoreDbContext

而链接字符串通过如下代码保存 DbContextCreationContext:

   using (DbContextCreationContext.Use(creationContext))
   {
   }

DbContextCreationContext 的代码如下:

public class DbContextCreationContext
{
    public static DbContextCreationContext Current => _current.Value!;
    private static readonly AsyncLocal<DbContextCreationContext> _current = new AsyncLocal<DbContextCreationContext>();

    public string ConnectionStringName { get; }

    public string ConnectionString { get; }

    public DbConnection? ExistingConnection { get; internal set; }

    public DbContextCreationContext(string connectionStringName, string connectionString)
    {
        ConnectionStringName = connectionStringName;
        ConnectionString = connectionString;
    }

    public static IDisposable Use(DbContextCreationContext context)
    {
        var previousValue = Current;
        _current.Value = context;
        return new DisposeAction(() => _current.Value = previousValue);
    }
}

然后是

public static class DbContextOptionsFactory
{
    public static DbContextOptions<TDbContext> Create<TDbContext>(IServiceProvider serviceProvider)
        where TDbContext : AbpDbContext<TDbContext>
    {
        var creationContext = GetCreationContext<TDbContext>(serviceProvider);

        var context = new AbpDbContextConfigurationContext<TDbContext>(
            creationContext.ConnectionString,
            serviceProvider,
            creationContext.ConnectionStringName,
            creationContext.ExistingConnection
        );

        var options = GetDbContextOptions<TDbContext>(serviceProvider);

        PreConfigure(options, context);
        Configure(options, context);

        return context.DbContextOptions.Options;
    }

    private static DbContextCreationContext GetCreationContext<TDbContext>(IServiceProvider serviceProvider)
        where TDbContext : AbpDbContext<TDbContext>
    {
        var context = DbContextCreationContext.Current;
        if (context != null)
        {
            return context;
        }

        var connectionStringName = ConnectionStringNameAttribute.GetConnStringName<TDbContext>();
        var connectionString = ResolveConnectionString<TDbContext>(serviceProvider, connectionStringName); 

        return new DbContextCreationContext(
            connectionStringName,
            connectionString
        );
    }

其中代码


这样 AbpDbContextConfigurationContext将保存的数据库链接字符串

在扩展类 AbpEfCoreServiceCollectionExtensions 添加上下文的扩展方法中 AddAbpDbContext

namespace Microsoft.Extensions.DependencyInjection;

public static class AbpEfCoreServiceCollectionExtensions
{
    public static IServiceCollection AddAbpDbContext<TDbContext>(
        this IServiceCollection services,
        Action<IAbpDbContextRegistrationOptionsBuilder>? optionsBuilder = null)
        where TDbContext : AbpDbContext<TDbContext>
    {
        .......
        services.TryAddTransient(DbContextOptionsFactory.Create<TDbContext>);
        ......
    }
}

其中:

services.TryAddTransient(DbContextOptionsFactory.Create<TDbContext>);

调用上面的定义的DbContextOptionsFactory.Create() 方法

在扩展方类AbpDbContextConfigurationContextSqlServerExtensions的扩展方法 UseSqlServer()

public static class AbpDbContextConfigurationContextSqlServerExtensions
{
    public static DbContextOptionsBuilder UseSqlServer(
        [NotNull] this AbpDbContextConfigurationContext context,
        Action<SqlServerDbContextOptionsBuilder>? sqlServerOptionsAction = null)
    {
        if (context.ExistingConnection != null)
        {
            return context.DbContextOptions.UseSqlServer(context.ExistingConnection, optionsBuilder =>
            {
                optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
                sqlServerOptionsAction?.Invoke(optionsBuilder);
            });
        }
        else
        {
            return context.DbContextOptions.UseSqlServer(context.ConnectionString, optionsBuilder =>
            {
                optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
                sqlServerOptionsAction?.Invoke(optionsBuilder);
            });
        }
    }
}

关键代码:

UseSqlServer(context.ConnectionString,...)

使用了 AbpDbContextConfigurationContext中保存的数据库链接字符串。

每次请求都会再调用一次 UseSqlServer()UseMySQL() ,并使用了当前租户的数据库链接字符串。