AspnetBoilerplate (ABP) Organization Units 组织结构管理
ABP是一个成熟的.NET框架,功能完善。目前由于项目需要正在自学中。
ABP对于组织节点管理这一基本上每个项目都要反复重复开发的内容,进行了自己的实现。
主要包括这些常用功能:
- 多租户
- 树结构管理的实体
- 与用户系统集成的查询
不过需要注意的是,ABP默认没有提供展示层的实现,这一块就需要自己实现了。
官方文档理解
OrganizationUnit 实体定义
- TenantId: 租户ID,如果为null则是host的组织节点。(具体概念参阅多租户)
- ParentId: 父节点Id,如果为null则是根节点。
- Code: 一个拼接的虚拟路径字符串代码,在租户内唯一。
- DisplayName: 显示名称
Organization Tree
模型定义中的ParentId使得这个数据结构定义了一个典型的父子树。
- 这个树允许有多个根节点
- 树的最大深度是OrganizationUnit.MaxDepth,值为16
- 每一级子节点的数目也有限制,主要是由于后面要提到的OU Code定义决定的。
OU Code
OU Code由OrganizationUnit Manager自动维护,它是类似于"00001.00042.00005"的字符串。它可以用于递归查询。
这种字段在树结构中是很必须的,如果没有它,树查询会变成效率的杀手。有了这类虚拟路径,可以通过分隔符分解后批量查询。
Abp对OU Code有以下规则:
- 在一个租户中唯一
- 子节点的Code需要以父节点的Code开头
- Code的长度,由层级深度决定
- OU Code可以被改变,例如移动节点
- 我们需要使用Id作为OU引用的字段,而不是Code
OrganizationUnit Manager
- OrganizationUnitManager 通过依赖注入引入,一般用于:
- 增、删、改OU
- 移动OU
- 读取OU信息,以及OU的items
Multi-Tenancy
OrganizationUnitManager 一次只能操作一个租户,默认租户为当前租户。
样例代码分析
首先创建一个实体,派生自IMustHaveTenant , IMustHaveOrganizationUnit
public class Product : Entity, IMustHaveTenant, IMustHaveOrganizationUnit { public virtual int TenantId { get; set; } public virtual long OrganizationUnitId { get; set; } public virtual string Name { get; set; } public virtual float Price { get; set; } }
实现Service:
public class ProductManager : IDomainService { //实体仓储,实体继承自IMustHaveOrganizationUnit private readonly IRepository<Product> _productRepository; //OU仓储,通过此仓储读取OU private readonly IRepository<OrganizationUnit, long> _organizationUnitRepository; //用户数据Manager private readonly UserManager _userManager; //构造函数,DI注入 public ProductManager( IRepository<Product> productRepository, IRepository<OrganizationUnit, long> organizationUnitRepository, UserManager userManager) { _productRepository = productRepository; _organizationUnitRepository = organizationUnitRepository; _userManager = userManager; } //根据组织节点,获取关联的Product public List<Product> GetProductsInOu(long organizationUnitId) { return _productRepository.GetAllList(p => p.OrganizationUnitId == organizationUnitId); } //根据组织节点Id查询所有的Products,包含子Product [UnitOfWork]//UnitOfWork支持事务 public virtual List<Product> GetProductsInOuIncludingChildren(long organizationUnitId) { //根据组织节点id,获取code var code = _organizationUnitRepository.Get(organizationUnitId).Code; //查询组织节点开头的所有节点,这样避免了递归查询,提升了效率,也是Code定义的目的所在 var query = from product in _productRepository.GetAll() join organizationUnit in _organizationUnitRepository.GetAll() on product.OrganizationUnitId equals organizationUnit.Id where organizationUnit.Code.StartsWith(code) select product; return query.ToList(); } //根据用户查询Product //查询用户关联的组织节点,再根据组织节点,查询关联的Product public async Task<List<Product>> GetProductsForUserAsync(long userId) { var user = await _userManager.GetUserByIdAsync(userId); var organizationUnits = await _userManager.GetOrganizationUnitsAsync(user); var organizationUnitIds = organizationUnits.Select(ou => ou.Id); return await _productRepository.GetAllListAsync(p => organizationUnitIds.Contains(p.OrganizationUnitId)); } //同上个函数类似,查询中加入了子节点 [UnitOfWork] public virtual async Task<List<Product>> GetProductsForUserIncludingChildOusAsync(long userId) { var user = await _userManager.GetUserByIdAsync(userId); var organizationUnits = await _userManager.GetOrganizationUnitsAsync(user); var organizationUnitCodes = organizationUnits.Select(ou => ou.Code); var query = from product in _productRepository.GetAll() join organizationUnit in _organizationUnitRepository.GetAll() on product.OrganizationUnitId equals organizationUnit.Id where organizationUnitCodes.Any(code => organizationUnit.Code.StartsWith(code)) select product; return query.ToList(); } }
通过这段源码我们发现,其实在Abp模板中Zero模块已经默认添加了用户与组织节点的关联,如下图:
OrganizationUnits表是一个父子树结构,表达了我们系统中所有需要以父子树表达的逻辑结构。
实体表User,Product通过一张关联表与组织节点关联,关联关系如E-R图所示。
在数据库中,abp并没有创建外键联系,这应该是为了高复用OU表。
其他设置
你可以通过 AbpZeroSettingNames.OrganizationUnits.MaxUserMembershipCount 来设置一个用户的最大OU关联数。
本文采用 知识共享署名 4.0 国际许可协议 进行许可