企业级工作流解决方案(十三)--集成Abp和ng-alain--数据库读写分离
说到程序里面数据库管理,无非就是两件事情,一是数据库操作,对于数据库的操作,各种程序语言都有封装,也就是所谓的ORM框架,.net 方向一般用得比较多和就是.net framework和dapper,abp里还集成了NHibernate,另外就是连接字符串的管理,简单的应用直接用一个数据库连接字符串就可以了,但是对于大型的应用,比如有多租户概念的系统,比如有一些分库分表需求的设计系统,那么连接字符串的管理将是非常复杂和核心的内容。
对于读写分离,大家应该比较熟悉,数据库层面,大型的关系型数据库都支持,这里的读写分离是指代码层面,针对DBA已经做好的数据库读写分离来管理数据库连接字符串。
Abp基本框架提供了最基础的数据库连接字符串管理,zero项目实现了多租户的数据库连接管理,即把每个租户的连接字符串存储在租户里面,对于每一个Uow操作,都会找租户的连接字符串,如果找到,就使用,没有找到,向上层找默认的连接字符串。代码如下:
////// Implements to dynamically resolve /// connection string for a multi tenant application. /// public class DbPerTenantConnectionStringResolver : DefaultConnectionStringResolver, IDbPerTenantConnectionStringResolver { /// /// Reference to the session. /// public IAbpSession AbpSession { get; set; } private readonly ICurrentUnitOfWorkProvider _currentUnitOfWorkProvider; private readonly ITenantCache _tenantCache; /// /// Initializes a new instance of the class. /// public DbPerTenantConnectionStringResolver( IAbpStartupConfiguration configuration, ICurrentUnitOfWorkProvider currentUnitOfWorkProvider, ITenantCache tenantCache) : base(configuration) { _currentUnitOfWorkProvider = currentUnitOfWorkProvider; _tenantCache = tenantCache; AbpSession = NullAbpSession.Instance; } public override string GetNameOrConnectionString(ConnectionStringResolveArgs args) { if (args.MultiTenancySide == MultiTenancySides.Host) { return GetNameOrConnectionString(new DbPerTenantConnectionStringResolveArgs(null, args)); } return GetNameOrConnectionString(new DbPerTenantConnectionStringResolveArgs(GetCurrentTenantId(), args)); } public virtual string GetNameOrConnectionString(DbPerTenantConnectionStringResolveArgs args) { if (args.TenantId == null) { //Requested for host return base.GetNameOrConnectionString(args); } var tenantCacheItem = _tenantCache.Get(args.TenantId.Value); if (tenantCacheItem.ConnectionString.IsNullOrEmpty()) { //Tenant has not dedicated database return base.GetNameOrConnectionString(args); } return tenantCacheItem.ConnectionString; } protected virtual int? GetCurrentTenantId() { return _currentUnitOfWorkProvider.Current != null ? _currentUnitOfWorkProvider.Current.GetTenantId() : AbpSession.TenantId; } }
那么我们要改造的地方其他就可以参照这个来管理连接字符串
还有一个问题,就是我们怎么让框架知道我们使用的是读库还是写库呢?
Abp里面,公开给用户控制Uow的,就是UnitOfWorkAttribute装饰器,增加一个读库还是写库的标识IsReadDb,在UnitOfWorkOptions类里面也要加对应的属性,那么我们在构造UnitOfWorkOptions类的时候,可以把属性装饰器里面的IsReadDb属性赋值给UnitOfWorkOptions,再获取DbContext方法的时候,把此参数传入Uow连接字符串管理,在连接字符串管理里面,判断此参数的值,确定数据库字符串选择。
主要代码:
////// Unit of work options. /// public class UnitOfWorkOptions { // ...... /// /// 自定义:设置是否是读库 /// public bool IsReadDb { get; set; } } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)] public class UnitOfWorkAttribute : Attribute { // ...... /// /// 自定义:设置是否是读库 /// public bool IsReadDb { get; set; } public UnitOfWorkOptions CreateOptions() { return new UnitOfWorkOptions { IsTransactional = IsTransactional, IsolationLevel = IsolationLevel, Timeout = Timeout, Scope = Scope, IsReadDb = IsReadDb }; } }
在获取DbContext方法的时候,传递数据库读写参数
public static class UnitOfWorkExtensions { public static TDbContext GetDbContext(this IActiveUnitOfWork unitOfWork, MultiTenancySides? multiTenancySide = null, string name = null) where TDbContext : DbContext { if (unitOfWork == null) { throw new ArgumentNullException("unitOfWork"); } if (!(unitOfWork is EfCoreUnitOfWork)) { throw new ArgumentException("unitOfWork is not type of " + typeof(EfCoreUnitOfWork).FullName, "unitOfWork"); } return (unitOfWork as EfCoreUnitOfWork).GetOrCreateDbContext (multiTenancySide, name, unitOfWork.Options.IsReadDb); } } public virtual TDbContext GetOrCreateDbContext (MultiTenancySides? multiTenancySide = null, string name = null,bool isReadDb = false) where TDbContext : DbContext { var concreteDbContextType = _dbContextTypeMatcher.GetConcreteType(typeof(TDbContext)); var connectionStringResolveArgs = new ConnectionStringResolveArgs(multiTenancySide); connectionStringResolveArgs["DbContextType"] = typeof(TDbContext); connectionStringResolveArgs["DbContextConcreteType"] = concreteDbContextType; connectionStringResolveArgs["IsReadDb"] = isReadDb; var connectionString = ResolveConnectionString(connectionStringResolveArgs); }
最终,参照Abp连接字符串的管理,代码如下:
public class DbPerTenantConnectionStringResolver : DefaultConnectionStringResolver, IDbPerTenantConnectionStringResolver { ////// Reference to the session. /// public IAbpSession AbpSession { get; set; } private readonly ICurrentUnitOfWorkProvider _currentUnitOfWorkProvider; /// /// Initializes a new instance of the class. /// public DbPerTenantConnectionStringResolver( IAbpStartupConfiguration configuration, ICurrentUnitOfWorkProvider currentUnitOfWorkProvider) : base( configuration) { _currentUnitOfWorkProvider = currentUnitOfWorkProvider; AbpSession = NullAbpSession.Instance; } public override string GetNameOrConnectionString(ConnectionStringResolveArgs args) { if (args.MultiTenancySide == MultiTenancySides.Host) { return GetNameOrConnectionString(new DbPerTenantConnectionStringResolveArgs(null, args)); } return GetNameOrConnectionString(new DbPerTenantConnectionStringResolveArgs(GetCurrentTenantId(), args)); } public virtual string GetNameOrConnectionString(DbPerTenantConnectionStringResolveArgs args) { if (args.TenantId == null) { //Requested for host return base.GetNameOrConnectionString(args); } var tenantCacheItem = Rpc.Call ("CommonService.CommonServiceServiceAppService.GetTenantInfo", args.TenantId); if(tenantCacheItem == null) { return base.GetNameOrConnectionString(args); } if(Convert.ToBoolean(args["IsReadDb"])) { if(!string.IsNullOrEmpty(tenantCacheItem.ReadConnectionString)) { return tenantCacheItem.ReadConnectionString; } else { return base.GetNameOrConnectionString(args); } } else { if (!string.IsNullOrEmpty(tenantCacheItem.ConnectionString)) { return tenantCacheItem.ConnectionString; } else { return base.GetNameOrConnectionString(args); } } } protected virtual int? GetCurrentTenantId() { return _currentUnitOfWorkProvider.Current != null ? _currentUnitOfWorkProvider.Current.GetTenantId() : AbpSession.TenantId; } }
使用的时候,在类或者方法上,增加Uow属性装饰器上定义参数即可
补充说明:可以参照这种方式,自定义的扩展,比如每一个DbContext自定义连接字符串,我们可以在自己的租户管理表中添加属性,自定义数据库连接字符串选择逻辑。