在我們開發業務的時候,一般數據庫表都有相關的關系,除了單獨表外,一般還包括一對多、多對多等常見的關系,在實際開發過程中,需要結合系統框架做對應的處理,本篇隨筆介紹基於ABP框架對EF實體、DTO關系的處理,以及提供對應的接口進行相關的數據保存更新操作。
1、一對多關系的數據處理
一對多,也可以叫做主從表的關系,其中從表有一個外鍵和主表進行關聯,如下所示。
上圖是一個簡單的主從表關系,其中客戶信息表只有簡單的一兩個字段用於演示,從表用來記錄對應客戶的地址信息。
其中表中的CreateUserId、CreateTime、LastModifierUserId、LastModificationTime、DeleterUserId、IsDeleted、DeletionTime、TenantId字段,是我們一般設計ABP表保留的字段。
我們先從一個關系圖來了解下框架下的領域驅動模塊中的各個類之間的關系。
ABP框架,和應用服務層或者界面層打交道的數據對象是DTO對象,和數據庫打交道的是實體對象,連接連接起來是通過AutoMapper實現映射處理,映射是通過映射文件進行配置,一般我們可以根據數據庫表信息進行生成DTO、Entity,以及映射文件。
以客戶及客戶地址表為例,生成的DTO對象如下所示。
/// <summary> /// 創建客戶信息,DTO對象 /// </summary> public class CreateCustomerDto : FullAuditedEntityDto<string> { /// <summary> /// 默認構造函數(需要初始化屬性的在此處理) /// </summary> public CreateCustomerDto() { this.Id = Guid.NewGuid().ToString(); } #region Property Members /// <summary> /// 姓名 /// </summary> [Required] public virtual string Name { get; set; } /// <summary> /// 年齡 /// </summary> //[Required] public virtual int? Age { get; set; } #endregion } /// <summary> /// 客戶信息,DTO對象 /// </summary> public class CustomerDto : CreateCustomerDto { }
/// <summary> /// 創建客戶地址簿,DTO對象 /// </summary> public class CreateCustomerAddressDto : CreationAuditedEntityDto<string> { /// <summary> /// 默認構造函數(需要初始化屬性的在此處理) /// </summary> public CreateCustomerAddressDto() { this.Id = System.Guid.NewGuid().ToString(); } #region Property Members /// <summary> /// 客戶ID /// </summary> //[Required] public virtual string Customer_ID { get; set; } /// <summary> /// 省份 /// </summary> //[Required] public virtual string Province { get; set; } /// <summary> /// 城市 /// </summary> //[Required] public virtual string City { get; set; } /// <summary> /// 區縣 /// </summary> //[Required] public virtual string District { get; set; } /// <summary> /// 詳細地址 /// </summary> //[Required] public virtual string DetailAddress { get; set; } /// <summary> /// 排序 /// </summary> //[Required] public virtual string SortCode { get; set; } #endregion } /// <summary> /// 客戶地址簿,DTO對象 /// </summary> public class CustomerAddressDto : CreateCustomerAddressDto { }
其表對應的實體類,也和DTO類似,不過是和數據庫打交道的數據對象
/// <summary> /// 客戶信息,領域對象 /// </summary> [Table("T_Customer")] public class Customer : FullAuditedEntity<string> { /// <summary> /// 默認構造函數(需要初始化屬性的在此處理) /// </summary> public Customer() { } #region Property Members /// <summary> /// 姓名 /// </summary> //[Required] public virtual string Name { get; set; } /// <summary> /// 年齡 /// </summary> //[Required] public virtual int? Age { get; set; } #endregion }
/// <summary> /// 客戶地址簿,領域對象 /// </summary> [Table("T_CustomerAddress")] public class CustomerAddress : CreationAuditedEntity<string> { /// <summary> /// 默認構造函數(需要初始化屬性的在此處理) /// </summary> public CustomerAddress() { } #region Property Members /// <summary> /// 客戶ID /// </summary> //[Required] public virtual string Customer_ID { get; set; } /// <summary> /// 省份 /// </summary> //[Required] public virtual string Province { get; set; } /// <summary> /// 城市 /// </summary> //[Required] public virtual string City { get; set; } /// <summary> /// 區縣 /// </summary> //[Required] public virtual string District { get; set; } /// <summary> /// 詳細地址 /// </summary> //[Required] public virtual string DetailAddress { get; set; } /// <summary> /// 排序 /// </summary> //[Required] public virtual string SortCode { get; set; } /// <summary> /// 客戶ID /// </summary> //[Required] [ForeignKey("Customer_ID")] public virtual Customer Customer { get; set; } #endregion }
映射文件如下所示。
/// <summary> /// 客戶信息,映射文件 /// </summary> public class CustomerMapProfile : Profile { public CustomerMapProfile() { CreateMap<CustomerDto, Customer>(); CreateMap<Customer, CustomerDto>(); CreateMap<CreateCustomerDto, Customer>(); } }
/// <summary> /// 客戶地址簿,映射文件 /// </summary> public class CustomerAddressMapProfile : Profile { public CustomerAddressMapProfile() { CreateMap<CustomerAddressDto, CustomerAddress>(); CreateMap<CustomerAddress, CustomerAddressDto>(); CreateMap<CreateCustomerAddressDto, CustomerAddress>(); } }
然后在EFCore的上下文中添加對應的DBSet對象即可。
有了這些,基於ABP框架的基礎上就可以實現數據的創建、更新提交了。
2、一對多關系的界面處理和服務端ABP接口的處理
但是主從表之間的關系,我們這里還沒有詳細說明,一般我們在界面處理數據的時候,主表數據可能和從表數據一起顯示,編輯的時候一起保存,如下界面所示。
在編輯/新增界面中綁定GridView的相關顯示和處理事件。
我們可以在新增窗口中加載空地址列表,或者編輯窗口加載已有地址列表記錄
保存新增的記錄如下所示。
/// <summary> /// 新增狀態下的數據保存 /// </summary> /// <returns></returns> public async override Task<bool> SaveAddNew() { CustomerDto info = tempInfo;//必須使用存在的局部變量,因為部分信息可能被附件使用 SetInfo(info); try { #region 新增數據 tempInfo = await CustomerApiCaller.Instance.CreateAsync(info); if (tempInfo != null) { //可添加其他關聯操作 var list = GetDetailList(); foreach(var detailInfo in list) { await CustomerAddressApiCaller.Instance.InsertOrUpdateAsync(detailInfo); } return true; } #endregion } catch (Exception ex) { LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.Message); } return false; }
其中GetDetailList是獲取編輯狀態下的數據記錄
/// <summary> /// 獲取明細列表 /// </summary> /// <returns></returns> private List<CustomerAddressDto> GetDetailList() { var list = new List<CustomerAddressDto>(); for (int i = 0; i < this.gridView1.RowCount; i++) { var detailInfo = gridView1.GetRow(i) as CustomerAddressDto; if (detailInfo != null) { list.Add(detailInfo); } } return list; }
如果數據更新的時候,操作也是類似
/// <summary> /// 編輯狀態下的數據保存 /// </summary> /// <returns></returns> public override async Task<bool> SaveUpdated() { CustomerDto info = await CustomerApiCaller.Instance.GetAsync(ID); if (info != null) { SetInfo(info); try { #region 更新數據 tempInfo = await CustomerApiCaller.Instance.UpdateAsync(info); if (tempInfo != null) { //可添加其他關聯操作 var list = GetDetailList(); foreach(var detailInfo in list) { await CustomerAddressApiCaller.Instance.InsertOrUpdateAsync(detailInfo); } return true; } #endregion } catch (Exception ex) { LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.Message); } } return false; }
我們這里注意到不管更新還是插入地址記錄,都用到了一個函數InsertOrUpdateAsync,這個是我們后台判斷記錄是新增或者更新,在寫入數據庫操作中的處理函數。
這個函數比較通用,我們可以考慮把它寫入公用的基類接口里面即可。
同樣,客戶端的ApiCaller調用,也需要進行一個簡單的基類接口增加即可。
有了這些支持,Winform客戶端的處理就可以直接調用這些簡單的接口進行主從表的數據提交了。
//可添加其他關聯操作 var list = GetDetailList(); foreach(var detailInfo in list) { await CustomerAddressApiCaller.Instance.InsertOrUpdateAsync(detailInfo); }
另外,除了這種細粒度的接口處理,我們還可以把整個DTO對象包裝一下,在主表DTO對象中包含從表明細列表,然后重寫Create、Update的服務端應用服務層接口,接收從表明細,然后一個接口就可以處理主從表的數據保存或者更新了。
具體如何選擇數據處理的方式,需要根據業務的場景進行衡量。
3、結合代碼生成工具實現后台代碼和主從表界面代碼的快速生成
一旦業務規則確定,我們可以運用代碼生成工具來提高開發效率了。由於主從表關系的處理比較統一,因此我們可以按照他們的關系以及界面常見的處理方式來生成這些內容。
首先,我們打開代碼生成工具,展開對應數據庫的表信息,如下界面所示。
選擇ABP框架代碼生成,可以生成后台框架代碼,其中包括DTO實體、實體對象、映射文件,服務端應用層接口和實現等內容。
生成Winform主從表界面的時候,選擇Winform代碼生成,如下界面所示。
然后在彈出的界面里面選擇主從表界面的生成選項卡即可。
為了方便讀者理解,我列出一下前面幾篇隨筆的連接,供參考:
循序漸進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)