软件即服务概念的推动,定制化到通用化的发展,用一套代码完成适应不同企业的需求,利用多租户技术可以去做到这一点。ABP里提供了多租户这一概念并且也在Zero模块中实现了这一概念。
一、多租户的概念
单部署-单数据库:部署应用程序的单个实例和单个数据库。在每个数据表(关系型数据库)里用一个TenantId(租户Id或类似的如企业Id)字段来隔离区分每个租户数据。
单部署-多数据库:部署应用的单一实例,用一个主(宿主)数据库存储租户元数据(像租户名和子域),并为每个租户建立并维护一个隔离的数据库。操作上通过识别当前租户并从主数据库中读取相对应的存储数据库地址,切换到该租户独有的数据库中执行操作。
多部署-多数据库:为每个租户部署应用的一个实例并使用一个独立的数据库,那么我们就可以在一台服务器上为多个租户服务,只需确保相同应用的多个实例在一个服务器的环境下不会互相冲突就行。
还有两种多租户形式,诸如单部署-混搭数据库、集群部署-单/多/混搭数据库,不再提及,ABP针对于这五种多租户形式都可以使用。
宿主与租户:对于这两个概念,最好的理解方式便是酒店,酒店老板即是宿主,租户只拥有房间权限,酒店老板可以提供管理所有租户和房间。
二、ABP中多租户配置
1、启用/禁用多租户
如果不需要多租户,比如说没有多租户情形,应用部署在企业私有服务器上,那么也可以不考虑多租户的使用,可以在ABP中关闭多租户(尽管关闭了,但默认还是会使用一个租户,默认租户Id为1,此时只有这个默认租户没有宿主)。在WebCoreModule中PreInitialize方法内可以添加如下代码启用或关闭多租户,默认是启用的。
public override void PreInitialize() { ... Configuration.MultiTenancy.IsEnabled = true; ... }
2、侦测当前租户并在Session中获取租户
ABP中租户名称是唯一的,对于识别当前租户或是宿主身份,ABP没有使用Asp.Net 提供的Session,声明了IAbpSession接口并提供了默认的实现(ClaimAbpSession)去测定当前租户信息,按照如下思路去确定租户。
- 如果当前用户登录了系统,那么可以从当前用户的声明信息中读取到当前租户信息,如果没有读取到租户信息,那么可以断定是宿主。
- 如果当前用户没有登录系统,那么会有几种方式去获取,如果以下几种方式仍未获取到租户Id,则认为是使用宿主登录。
- 从当前域名或是子域名去获取域名名称,然后通过租户仓储去查询是否存在相关的域名或子域名存在则可以确定租户Id。
- 从Http请求头中获取在ABP中默认配置项Abp.TenantId(该配置项可更换名称)。
- 从Http请求的cookie中获取Abp.TenantId
IAbpSession声明了获取当前用户和租户信息的方法,该方法允许我们获取当前登录的用户及当前的租户信息。并且获取到的信息按照不同的规则,有着不同的作用。
- 如果获取到的用户和租户Id都是空的,那么意味着当前用户没有登录系统,因此也无法断定出当前是宿主还是租户。
- 如果用户Id是空的,但是租户Id不是空的,那么可以知道是哪个租户,但是用户仍然是没有登录的,只是选择了租户。
- 如果用户Id不是空的,但是租户Id是空的,那么可以知道是用户使用宿主登录了系统。
- 如果用户Id且租户Id不是空的,那么就知道是选择了租户并且是租户中的某个用户登录了系统。
3、数据过滤
如果使用了多租户,那么在读取数据时,会依据当前租户Id加上额外的过滤条件,这一点ABP已经处理好了,我们无需在linq中敲代码,但是有个前提条件是,读取数据的这个实体有设置多租户。
1、如果实体使用的是IMustHaveTenant接口,那么读取时会依照当前租户Id进行条件过滤。
2、如果实体使用的是IMayHaveTenant接口,那么读取到的数据会依照当前租户Id的有无值进行区分,如果当前租户Id为空,那么将读取到宿主的数据,如果租户Id不为空,则读取相应租户数据。
3、如果实体没使用这两个接口,则读取到的数据不区分宿主和租户。
这两个接口使用场景:如果是宿主和租户都需要的,比如角色、用户、部门等,那么使用IMayHaveTenant接口,如果仅是租户所需要的那只需使用IMustHaveTenant接口。
4、宿主与租户间切换
此处切换可以这么理解,给我一个其它租户Id,我可以在我的租户中获取到其它租户的数据,相应的,其它租户也可以获取到我租户的数据,或是宿主获取租户数据。如果不给定租户Id,租户可以获取宿主数据。
public class ProductService : ITransientDependency { private readonly IRepository_productRepository; private readonly IUnitOfWorkManager _unitOfWorkManager; public ProductService(IRepository productRepository, IUnitOfWorkManager unitOfWorkManager) { _productRepository = productRepository; _unitOfWorkManager = unitOfWorkManager; } [UnitOfWork] public virtual List GetProducts(int tenantId) { using (_unitOfWorkManager.Current.SetTenantId(tenantId)) { return _productRepository.GetAllList(); } } }
三、配置一个多租户实体
基于之前的数据字典进行改造,以便适用于多个租户使用,并且考虑到宿主无需使用数据字典,将其继承IMustHaveTenant接口后,更新数据库便可。
public class DataDictionary : Entity<long>, IMustHaveTenant { public const int MaxNameLength = 30; ////// 租户Id /// public int? TenantId { get; set; } /// /// 字典类型 /// [StringLength(MaxNameLength)] public string TypeName { get; set; } /// /// 关联数据字典项 /// public virtual ICollection DataDictionaryItem { get; set; } }
我设置了默认当前租户,并且不提供选择租户的页面,通过Url去区分宿主和默认租户,登录当前租户账号后,查看当前网站内的数据字典看到之前已有数据全部消失,获取数据字典数据时,默认是ABP将当前租户Id带入作为查询条件了。
修改添加方法,添加当前租户Id的赋值,再次为当前租户添加几条相应的数据字典,页面中即可存在相关数据了。
dataDictionary.TenantId = AbpSession.TenantId.Value;
最后两条数据是指定租户Id为1下的,之前的数据可以手动清除。
至此,对于ABP中租户的相关使用了解的清楚了,对于多租户下数据存储量大,拆分成集群部署-多数据库情形没有做尝试,希望有机会可以使用一番。
仓库地址:https://gitee.com/530521314/Partner.Surround.git
2020-01-11,望技术有成后能回来看见自己的脚步