在我们开发业务的时候,一般数据库表都有相关的关系,除了单独表外,一般还包括一对多、多对多等常见的关系,在实际开发过程中,需要结合系统框架做对应的处理,本篇随笔介绍基于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)