资料
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()
将调用实现类 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))
{
}
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()
,并使用了当前租户的数据库链接字符串。