資料
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
的實現類 MultiTenantConnectionStringResolver
的 ResolveAsync()
方法,如下代碼所示:
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);
}
}
這樣就獲得了多租戶的數據庫鏈接字符串;
獲取數據庫上下文
回到 該接口實現類:UnitOfWorkDbContextProvider
的 GetDbContextAsync()
方法:
關鍵代碼:
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
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()
,保證了每次調用都是當前租戶的數據庫鏈接字符串。