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
的实现类 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()
,保证了每次调用都是当前租户的数据库链接字符串。