­

ABP框架中一對多,多對多關係的處理以及功能介面的處理(1)

在我們開發業務的時候,一般資料庫表都有相關的關係,除了單獨表外,一般還包括一對多、多對多等常見的關係,在實際開發過程中,需要結合系統框架做對應的處理,本篇隨筆介紹基於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程式碼生成,如下介面所示。

然後在彈出的介面裡面選擇主從表介面的生成選項卡即可。