1. 程式人生 > >ABP框架中一對多,多對多關係的處理以及功能介面的處理(1)

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程式碼生成,如下介面所示。

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

&n