11

ABP框架中一对多,多对多关系的处理以及功能界面的处理(2)

 3 years ago
source link: https://www.cnblogs.com/wuhuacong/p/13952849.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上篇随笔《ABP框架中一对多,多对多关系的处理以及功能界面的处理(1)》介绍了一对多关系下的主从表数据处理,包括ABP框架对EF实体、DTO等关系处理,以及应用层基类接口的调整和Apicaller调用层的封装,最后介绍了基于代码生成工具快速生成所需ABP框架代码和Winform界面代码的过程。

本篇基于ABP框架的基础上,继续介绍多对多关系的数据库设计、框架代码生成和调整,以实现常见多对多关系的数据处理。

1、多对多关系的数据库设计和界面关系

一般多对多的关系是指两个业务表之间存在关联关系,它们通过中间表(包含两个表的外键关系)建立多对多的关系,ABP框架除了两个外键关系外,一般还会增加几个系统字段,如下所示。

8867-20201110111641074-52632224.png

角色包含菜单资源也是多对多的关系,一般在角色新增或者编辑界面中进行维护。

8867-20201112092625016-948091529.png
8867-20201110111843585-994057656.png

功能界面设计的时候,就需要考虑和这些表之间的关系维护,如商品类型中,基本信息里面和品牌关系进行绑定。

8867-20201111114606928-1770533432.png

不管上面的树形列表,还是很后面的复选框组,都是先请求关联主表的数据,然后再请求对应角色或者商品类型下的关系数据,绑定到界面上。

如对于上面的树形列表,通过设置树列表的数据,以及选中的记录就可以实现对应关系的绑定。

    <el-tree
      ref="tree"
      class="filter-tree"
      style="padding-top:10px"
     :data="treedata"
      node-key="id"
      icon-class="el-icon-price-tag"
      default-expand-all
      highlight-current
      :show-checkbox="showcheck"
      :filter-node-method="filterNode"
    :default-checked-keys="checkedList"
    >

因此,在树形列表绑定的时候,需要请求原有的全部菜单数据,以及属于该角色下的菜单数据,两相整合就可以实现复选框选中已有菜单的效果了。

    async getlist() { // 树列表数据获取
      // 获取全部功能列表
      var param = { SkipCount: 0, MaxResultCount: 1000, Tag: 'web' }
      var treeList = [] // 所有功能列表
      await menu.GetAll(param).then(data => {
        treeList = data.result.items
      })
      // console.log(treeList)

      // 获取角色菜单列表
      var grantedList = []
      if (this.roleId && typeof (this.roleId) !== 'undefined') {
        param = { RoleId: this.roleId, MaxResultCount: 1000, MenuTag: 'web' }
        await role.GetMenusInRole(param).then(data => {
          grantedList = data.result.items
        })
      }
      // console.log(grantedList)
8867-20201112093724121-1186107375.png

 当然我们也可以把角色包含菜单数据放在角色对象的DTO里面,然后一次性就可以获得菜单集合了,如我这里介绍的商品类型中的包含的品牌列表做法一样。

2、ABP后端对于多对多关系的处理

多对多关系,是我们业务表常见的一种关系,如果是只读的展示,我们直接通过关联关系获得记录展示即可;如果是进行编辑的处理,那么需要获取关联主表的全部记录进行展示,然后根据关联关系,显示复选框勾中的记录展示。

刚才说到,我们商品类型中对于多对多的关系,可以通过后端直接返回对应的数据记录集合的,这种做法可以避免细粒度API的请求过程,不过对于太大的数据集合,建议还是通过单独的API进行获取。

我们为了在商品类型中返回相关品牌信息,那么需要定义一个简单的对象用来承载品牌信息,如下DTO所示。

    /// <summary>
    /// 品牌简单信息
    /// </summary>
    public class BrandItemDto
    {
        /// <summary>
        /// 品牌ID
        /// </summary>
        public virtual long Id { get; set; }

        /// <summary>
        /// 品牌编码
        /// </summary>
        public virtual string BrandCode { get; set; }

        /// <summary>
        /// 品牌名称
        /// </summary>
        public virtual string BrandName { get; set; }
    }

这个DTO是我们自定义的,我们需要映射常规的品牌DTO对象到这个自定义的DTO里面,那么我们可以通过映射文件中加入对应的映射关系来处理,避免属性的一一复制,如下所示。

8867-20201112094742374-300987273.png

然后,就是我们在商品类型中使用这个DTO的集合了,如下所示。

8867-20201112094928615-1959890958.png

我们知道,我们所有业务对象提供服务,都是通过对应的应用层服务接口提供,而商品类型这里对应的应用服务层对象是ProductTypeAppService,它继承自MyAsyncServiceBase基类对象,MyAsyncServiceBase基类对象重写了一些常规的方法,以便提供更方便的服务接口。

8867-20201112095727654-529179687.png

其中为了数据对象的转换方便,我们重写了Get和GetAll的方法,并提供一个通用的模板方法用来修改对象DTO的关系,如下代码所示。

8867-20201112095851829-1335424267.png

其中ConvertDto方法就是我们给子类重写,以便实现数据转换关系的。例如,我们在子类ProductTypeAppService里面重写了ConvertDto方法。

        /// <summary>
        /// 对记录进行转义
        /// </summary>
        /// <param name="item">dto数据对象</param>
        /// <returns></returns>
        protected override void ConvertDto(ProductTypeDto item)
        {
            //重写ConvertDto方法,返回其他关系数据
            var bindedBrands = GetBindedBrands(item.Id).Result.Items;
            //获取关联品牌的ID列表
            var brandIds = bindedBrands.Select(s => s.Id).ToArray();

            //获取关联品牌的对象列表
            var brandDtos = bindedBrands.Select(ObjectMapper.Map<BrandItemDto>).ToList();

            item.BindBrands = brandIds;     //纯ID集合
            item.BindBrandItems = brandDtos;//ID,BrandName,BrandCode 信息集合
        }

弄好了这些,我们测试接口,可以正确获得对应的记录列表了。

8867-20201112100342194-1761230324.png

这样我们就可以在列表或者编辑界面里都展示对应的关系了。

在列表展示界面中绑定已有关系代码如下所示。

  <el-table-column align="center" label="绑定品牌列表">
    <template slot-scope="scope">
      <el-tag
        v-for="opt in scope.row.bindBrandItems"
        :key="opt.id"
        type="primary"
        :disable-transitions="false"
      >
        {{ opt.brandName }}
      </el-tag>
    </template>
  </el-table-column>

在编辑界面中绑定已有关系代码如下所示。

  <el-form-item label="品牌关联" prop="bindBrands">
    <el-checkbox-group v-model="editForm.bindBrands" style="padding:10px;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1)">
      <el-checkbox v-for="(item, i) in brandList" :key="i" :label="item.id">{{ item.brandName }}</el-checkbox>
    </el-checkbox-group>
  </el-form-item>

其中editForm.bindBrands是我们包含的关系,而brandList这是所有品牌列表,这个需要在页面创建的时候,单独获取。

最后,需要介绍一下数据提交的时候,我们需要根据绑定列表关系,修改数据库已有的关联记录,这样实现关联关系的更新。

我们来看看创建商品类型和更新商品类型的时候,对关系数据的处理。

        /// <summary>
        /// 重写创建操作,写入额外的信息
        /// </summary>
        /// <param name="input">商品类型对象DTO</param>
        /// <returns></returns>
        public override async Task<ProductTypeDto> CreateAsync(CreateProductTypeDto input)
        {
            CheckCreatePermission();
            var entity = MapToEntity(input);

            await Repository.InsertAsync(entity);
            await CurrentUnitOfWork.SaveChangesAsync();

            //写入中间表关系
            if (input.BindBrands != null)
            {
                foreach (var brandId in input.BindBrands)
                {
                    //增加新增的
                    await _brandTypeRepository.InsertAsync(new BrandType(AbpSession.TenantId, brandId, entity.Id));
                }
            }

            return MapToEntityDto(entity);
        }

        /// <summary>
        /// 重写更新操作,更新新的关系数据
        /// </summary>
        /// <param name="input">商品类型对象DTO</param>
        /// <returns></returns>
        public override async Task<ProductTypeDto> UpdateAsync(ProductTypeDto input)
        {
            //保存主记录
            var dto = await base.UpdateAsync(input);

            //写入中间表关系
            if (input.BindBrands != null)
            {
                var brandsDto = new BrandsToProductTypeDto() { BrandIds = input.BindBrands, ProductTypeId = input.Id };
                await AddBrandToType(brandsDto);
            }

            return dto;
        }

其中 AddBrandToType 就是修改已有的品牌关系,在介绍这个函数开始前,先来看看商品类型应用服务层的定义,引入了商品类型、品牌、商品类型和品牌关系表三者的仓储对象作为参数的。

    /// <summary>
    /// 商品类型,应用层服务接口实现
    /// </summary>
    [AbpAuthorize]
    public class ProductTypeAppService : MyAsyncServiceBase<ProductType, ProductTypeDto, long, ProductTypePagedDto, CreateProductTypeDto, ProductTypeDto>, IProductTypeAppService
    {
        private readonly IRepository<ProductType, long> _repository;//业务对象仓储对象
        private readonly IRepository<User, long> _userRepository;//用户信息仓储对象
        private readonly IRepository<BrandType, long> _brandTypeRepository;//品牌分类中间表对象仓储对象
        private readonly IRepository<Brand, long> _brandRepository;//业务对象仓储对象

        public ProductTypeAppService(IRepository<ProductType, long> repository, IRepository<BrandType, long> brandTypeRepository, IRepository<Brand, long> brandRepository, IRepository<User, long> userRepository) : base(repository)
        {
            _repository = repository;
            _brandTypeRepository = brandTypeRepository;
            _brandRepository = brandRepository;
            _userRepository = userRepository;
        }

其中 AddBrandToType 需要修改关系,那么它的逻辑就是:如果不在新列表中的,移除数据库中的关系;如果新列表记录已在数据库中存在则跳过,否则写入关系。

详细代码如下所示,这个也是我们处理中间表之间关系的常见处理逻辑了。

        /// <summary>
        /// 添加品牌到分类
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public async Task AddBrandToType(BrandsToProductTypeDto input)
        {
            var typeInfo = Repository.GetAsync(input.ProductTypeId);
            if (typeInfo != null)
            {
                //获取与中间表联合的查询表达式
                var query = from cb in _brandTypeRepository.GetAll()
                            join b in _brandRepository.GetAll() on cb.Brand_ID equals b.Id
                            where cb.ProductType_ID == input.ProductTypeId
                            select b;

                var oldNotInNewList = query.Where(p => !input.BrandIds.Contains(p.Id)).ToList();
                foreach (var info in oldNotInNewList)
                {
                    //移除已有,但不在添加列表中的
                    await _brandTypeRepository.DeleteAsync(m => m.ProductType_ID == input.ProductTypeId && m.Brand_ID == info.Id);
                }

                if (input.BrandIds != null)
                {
                    //获取已有绑定列表
                    var currentBrands = query.ToList();
                    foreach (var brandid in input.BrandIds)
                    {
                        if (currentBrands.Any(cr => cr.Id == brandid))
                        {
                            continue; //已有重复的跳过
                        }

                        //否则增加新增的
                        await _brandTypeRepository.InsertAsync(new BrandType(AbpSession.TenantId, brandid, input.ProductTypeId));
                    }
                }
            }
        }

这样我们在商品类型编辑界面中可以随时变更关联关系了。

以上就是关于中间表的常见处理操作,希望对你学习ABP框架或者Element前端界面有所帮助。 

为了方便读者理解,我列出一下前面几篇随笔的连接,供参考:

循序渐进VUE+Element 前端应用开发(1)--- 开发环境的准备工作

循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用

循序渐进VUE+Element 前端应用开发(3)--- 动态菜单和路由的关联处理

循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理

循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理

循序渐进VUE+Element 前端应用开发(6)--- 常规Element 界面组件的使用

循序渐进VUE+Element 前端应用开发(7)--- 介绍一些常规的JS处理函数

循序渐进VUE+Element 前端应用开发(8)--- 树列表组件的使用

循序渐进VUE+Element 前端应用开发(9)--- 界面语言国际化的处理

循序渐进VUE+Element 前端应用开发(10)--- 基于vue-echarts处理各种图表展示 

循序渐进VUE+Element 前端应用开发(11)--- 图标的维护和使用

循序渐进VUE+Element 前端应用开发(12)--- 整合ABP框架的前端登录处理

循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理

循序渐进VUE+Element 前端应用开发(14)--- 根据ABP后端接口实现前端界面展示

循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理

循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理 

循序渐进VUE+Element 前端应用开发(17)--- 菜单管理

循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制  

循序渐进VUE+Element 前端应用开发(19)--- 后端查询接口和Vue前端的整合

循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码  

循序渐进VUE+Element 前端应用开发(21)--- 省市区县联动处理的组件使用

循序渐进VUE+Element 前端应用开发(22)--- 简化main.js处理代码,抽取过滤器、全局界面函数、组件注册等处理逻辑到不同的文件中 

循序渐进VUE+Element 前端应用开发(23)--- 基于ABP实现前后端的附件上传,图片或者附件展示管理

循序渐进VUE+Element 前端应用开发(24)--- 修改密码的前端界面和ABP后端设置处理 

循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1)

循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2) 

ABP框架中一对多,多对多关系的处理以及功能界面的处理(1) 

电商商品数据库的设计和功能界面的处理  

ABP框架中一对多,多对多关系的处理以及功能界面的处理(2)

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK