spritekuang

导航

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

统计

企业级工作流解决方案(十三)--集成Abp和ng-alain--数据库读写分离

  说到程序里面数据库管理,无非就是两件事情,一是数据库操作,对于数据库的操作,各种程序语言都有封装,也就是所谓的ORM框架,.net 方向一般用得比较多和就是.net framework和dapper,abp里还集成了NHibernate,另外就是连接字符串的管理,简单的应用直接用一个数据库连接字符串就可以了,但是对于大型的应用,比如有多租户概念的系统,比如有一些分库分表需求的设计系统,那么连接字符串的管理将是非常复杂和核心的内容。

  对于读写分离,大家应该比较熟悉,数据库层面,大型的关系型数据库都支持,这里的读写分离是指代码层面,针对DBA已经做好的数据库读写分离来管理数据库连接字符串。

  Abp基本框架提供了最基础的数据库连接字符串管理,zero项目实现了多租户的数据库连接管理,即把每个租户的连接字符串存储在租户里面,对于每一个Uow操作,都会找租户的连接字符串,如果找到,就使用,没有找到,向上层找默认的连接字符串。代码如下:

复制代码
/// <summary>
    /// Implements <see cref="IDbPerTenantConnectionStringResolver"/> to dynamically resolve
    /// connection string for a multi tenant application.
    /// </summary>
    public class DbPerTenantConnectionStringResolver : DefaultConnectionStringResolver, IDbPerTenantConnectionStringResolver
    {
        /// <summary>
        /// Reference to the session.
        /// </summary>
        public IAbpSession AbpSession { get; set; }

        private readonly ICurrentUnitOfWorkProvider _currentUnitOfWorkProvider;
        private readonly ITenantCache _tenantCache;

        /// <summary>
        /// Initializes a new instance of the <see cref="DbPerTenantConnectionStringResolver"/> class.
        /// </summary>
        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连接字符串管理,在连接字符串管理里面,判断此参数的值,确定数据库字符串选择。

  主要代码:

复制代码
/// <summary>
    /// Unit of work options.
    /// </summary>
    public class UnitOfWorkOptions
    {
        // ......

        /// <summary>
        /// 自定义:设置是否是读库
        /// </summary>
        public bool IsReadDb { get; set; }
}

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)]
    public class UnitOfWorkAttribute : Attribute
    {
        // ......

        /// <summary>
        /// 自定义:设置是否是读库
        /// </summary>
        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<TDbContext>(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<TDbContext>(multiTenancySide, name, unitOfWork.Options.IsReadDb);
        }
}

public virtual TDbContext GetOrCreateDbContext<TDbContext>(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
    {
        /// <summary>
        /// Reference to the session.
        /// </summary>
        public IAbpSession AbpSession { get; set; }

        private readonly ICurrentUnitOfWorkProvider _currentUnitOfWorkProvider;

        /// <summary>
        /// Initializes a new instance of the <see cref="DbPerTenantConnectionStringResolver"/> class.
        /// </summary>
        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<Sys_TenantQueryDto>("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自定义连接字符串,我们可以在自己的租户管理表中添加属性,自定义数据库连接字符串选择逻辑。

 

posted on   spritekuang  阅读(1529)  评论(3编辑  收藏  举报

编辑推荐:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体
点击右上角即可分享
微信分享提示