在我們開發業務的時候,一般數據庫表都有相關的關系,除了單獨表外,一般還包括一對多、多對多等常見的關系,在實際開發過程中,需要結合系統框架做對應的處理,本篇隨筆介紹基於ABP框架對EF實體、DTO關系的處理,以及提供對應的接口進行相關的數據保存更新操作,這篇介紹多對多關系下的ABP框架的處理。
上篇隨筆《ABP框架中一對多,多對多關系的處理以及功能界面的處理(1)》介紹了一對多關系下的主從表數據處理,包括ABP框架對EF實體、DTO等關系處理,以及應用層基類接口的調整和Apicaller調用層的封裝,最后介紹了基於代碼生成工具快速生成所需ABP框架代碼和Winform界面代碼的過程。
本篇基於ABP框架的基礎上,繼續介紹多對多關系的數據庫設計、框架代碼生成和調整,以實現常見多對多關系的數據處理。
1、多對多關系的數據庫設計和界面關系
一般多對多的關系是指兩個業務表之間存在關聯關系,它們通過中間表(包含兩個表的外鍵關系)建立多對多的關系,ABP框架除了兩個外鍵關系外,一般還會增加幾個系統字段,如下所示。
角色包含菜單資源也是多對多的關系,一般在角色新增或者編輯界面中進行維護。
或者
功能界面設計的時候,就需要考慮和這些表之間的關系維護,如商品類型中,基本信息里面和品牌關系進行綁定。
不管上面的樹形列表,還是很后面的復選框組,都是先請求關聯主表的數據,然后再請求對應角色或者商品類型下的關系數據,綁定到界面上。
如對於上面的樹形列表,通過設置樹列表的數據,以及選中的記錄就可以實現對應關系的綁定。
<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)
當然我們也可以把角色包含菜單數據放在角色對象的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里面,那么我們可以通過映射文件中加入對應的映射關系來處理,避免屬性的一一復制,如下所示。
然后,就是我們在商品類型中使用這個DTO的集合了,如下所示。
我們知道,我們所有業務對象提供服務,都是通過對應的應用層服務接口提供,而商品類型這里對應的應用服務層對象是ProductTypeAppService,它繼承自MyAsyncServiceBase基類對象,MyAsyncServiceBase基類對象重寫了一些常規的方法,以便提供更方便的服務接口。
其中為了數據對象的轉換方便,我們重寫了Get和GetAll的方法,並提供一個通用的模板方法用來修改對象DTO的關系,如下代碼所示。
其中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 信息集合 }
弄好了這些,我們測試接口,可以正確獲得對應的記錄列表了。
這樣我們就可以在列表或者編輯界面里都展示對應的關系了。
在列表展示界面中綁定已有關系代碼如下所示。
<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)