Abp vNext:多租戶如何切換數據庫

資料

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

多租戶的數據庫架構

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.

多租戶如何切換數據庫

基礎倉儲(比如:EfCoreRepository)中依賴注入 IDbContextProvider<TDbContext>: where TDbContext : IEfCoreDbContext

namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore;
public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IEfCoreRepository<TEntity>
    where TDbContext : IEfCoreDbContext
    where TEntity : class, IEntity
{

    private readonly IDbContextProvider<TDbContext> _dbContextProvider;

    public EfCoreRepository(IDbContextProvider<TDbContext> dbContextProvider)
    {
        _dbContextProvider = dbContextProvider;
    }

    protected virtual Task<TDbContext> GetDbContextAsync()
    {
        // Multi-tenancy unaware entities should always use the host connection string
        if (!EntityHelper.IsMultiTenant<TEntity>())
        {
            using (CurrentTenant.Change(null))
            {
                return _dbContextProvider.GetDbContextAsync();
            }
        }

        return _dbContextProvider.GetDbContextAsync();
    }
    ......
    // 表
    protected async Task<DbSet<TEntity>> GetDbSetAsync()
    {
        return (await GetDbContextAsync()).Set<TEntity>();
    }
    // 查詢
    public async override Task<IQueryable<TEntity>> GetQueryableAsync()
    {
        return (await GetDbSetAsync()).AsQueryable().AsNoTrackingIf(!ShouldTrackingEntityChange());
    }
    // 保存
    protected async override Task SaveChangesAsync(CancellationToken cancellationToken)
    {
        await (await GetDbContextAsync()).SaveChangesAsync(cancellationToken);
    }
    ......

而獲取數據上下文接口 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;
    }
  ......
}

關鍵代碼:

var connectionString = await ResolveConnectionStringAsync(connectionStringName);

解析當前租戶(包括Host)數據庫鏈接字符串。

獲取租戶數據庫鏈接字符串

UnitOfWorkDbContextProvider<TDbContext> : IDbContextProvider<TDbContext> 的接口實現方法 GetDbContextAsync() 的關鍵代碼如下:

  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() 將調用接口 IConnectionStringResolver 的實現類 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))      // 先切換到當前的數據庫配置(包括數據鏈接,鏈接字符串等)
   {
      var dbContext = await CreateDbContextAsync(unitOfWork); // 然後創建 DbContext
      ......
   }

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);
    }
}

其中,

  • 注意變量 DbContextCreationContext _current:
  public static DbContextCreationContext Current => _current.Value!;
  private static readonly AsyncLocal<DbContextCreationContext> _current = new AsyncLocal<DbContextCreationContext>();

AsyncLocal 是一個異步上下文(async context)相關的類,它提供了在異步調用鏈(如異步方法、任務)中共享數據的功能。
static 關鍵字表示該變量是靜態的,即在類的所有實例之間共享。靜態變量只會在內存中創建一次,類的所有實例都可以訪問並共享相同的值。在這種情況下,_current 變量將在該類的所有實例之間共享。
這樣的定義,我們可以創建一個只能在類內部訪問、被類的所有實例共享、不可更改的異步上下文變量 _current,用於在異步調用鏈中共享 DbContextCreationContext 數據。
然後通外部通過 DbContextCreationContext Current 變量獲取

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

Use()方法保證了,當在Use範圍後調用 IDisposable() 方法,將恢復使用之前的值,即:Use()內切換爲當前租戶的數據庫配置,Use() 結束後恢復Host的數據庫配置,

然後是

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
        );
    }

其中代碼

        var creationContext = GetCreationContext<TDbContext>(serviceProvider);

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

將當前租戶的 DbContextCreationContext.ConnectionString 傳給了 AbpDbContextConfigurationContext.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() 方法,創建了 AbpDbContextConfigurationContext 類實例。

在擴展方類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中保存的數據庫鏈接字符串。

最後
在 EFCore 層中配置數據庫:

    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddAbpDbContext<AdministrationServiceDbContext>(options =>
        {
            options.AddDefaultRepositories(includeAllEntities: true);
        });

        Configure<AbpDbContextOptions>(options =>
        {
            options.Configure<AdministrationServiceDbContext>(c =>
            {
                c.UseSqlServer();
            });

        });
    }
  • AddAbpDbContext<AdministrationServiceDbContext() : 創建了 AbpDbContextConfigurationContext類實例,並使用當前租戶(包括Host)的數據庫鏈接字符串。
  • UseSqlServer(): 使用AbpDbContextConfigurationContext類實例中保存的當前租戶的數據庫鏈接字符串創建數據上下文。
  • 每次請求都會再調用一次 UseSqlServer()UseMySQL() ,保證了每次調用都是當前租戶的數據庫鏈接字符串。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章