1、回顾项目的架构和模块内容
在前面几篇随笔中,大概介绍过了基于SqlSugar的开发框架主要的设计模块,场景如下所示。
基础核心数据模块SugarProjectCore,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。其中Service里面一些框架基类和接口定义,统一也放在公用类库里面。
Winform界面,我们可以采用基于.net Framework开发或者.net core6进行开发均可,因为我们的SugarProjectCore项目是采用.net Standard模式开发,兼容两者。
这里以权限模块来进行演示整合使用,我在构建代码生成工具代码模板的时候,反复利用项目中测试没问题的项目代码指导具体的模板编写,这样编写出来的模板就会完美符合实际的项目需要了。
在项目代码及模板完成后,利用代码生成工具快速生成代码,相互促进情况下,也完成了Winform项目的界面代码生成,生成包括普通的列表界面,以及主从表Winform界面代码生成。
最后权限系统的Winform项目如下所示。
在前面随笔《基于SqlSugar的开发框架循序渐进介绍(2)– 基于中间表的查询处理》中介绍了基础功能的一些处理,其中也介绍到了Winform界面端的界面效果,这个以SqlSugar底层处理,最终把权限、字典等模块整合到一起,完成一个项目开发所需要的框架结构内容。整个系统包括用户管理、组织机构管理、角色管理、功能权限管理、菜单管理、字段权限管理、黑白名单、操作日志、字典管理、客户信息等模块内容。
在代码生成工具中,我们整合了基于SqlSugar的开发框架的项目代码生成,包括框架基础的代码生成,以及Winform界面代码生成两个部分,框架项目及Winform界面效果如上图所示。
2、整合代码生成工具Database2Sharp进行SqlSugar框架代码生成
前面随笔介绍过基于SqlSugar核心Core项目的组成。
基础核心数据模块SugarProjectCore,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。
对于Modal层的类代码生成,常规的普通表(非中间表),我们根据项目所需要,生成如下代码。目的是利用它定义好对应的主键Id,并通过接口约束实体类。
/// <summary> /// 客户信息 /// 继承自Entity,拥有Id主键属性 /// </summary> [SugarTable("T_Customer")] public class CustomerInfo : Entity<string>
而对于中间表,我们不要它的继承继承关系。
/// <summary> /// 用户角色关联 /// </summary> [SugarTable("T_ACL_User_Role")] public class User_RoleInfo { }
只需要简单的标注好SugarTable属性,让他可以和其他业务表进行关联查询即可。
/// <summary> /// 根据用户ID获取对应的角色列表 /// </summary> /// <param name="userID">用户ID</param> /// <returns></returns> private async Task<List<RoleInfo>> GetByUser(int userID) { var query = this.Client.Queryable<RoleInfo, User_RoleInfo>( (t, m) => t.Id == m.Role_ID && m.User_ID == userID) .Select(t => t); //联合条件获取对象 query = query.OrderBy(t => t.CreateTime);//排序 var list = await query.ToListAsync();//获取列表 return list; }
对于接口层的类,我们只需要按固定的继承关系处理好,以及类的名称变化即可。
/// <summary> /// 系统用户信息,应用层服务接口定义 /// </summary> public interface IUserService : IMyCrudService<UserInfo, int, UserPagedDto>, ITransientDependency { }
其中 IMyCrudService 是我们定义的基类接口,保存常规的增删改查等的处理基类,通过传入泛型进行约束接口参数类型和返回值。
基类接口尽可能满足实际项目接口所需,这样可以减少子类的代码编写,以及获得统一调用基类函数的便利。
对于中间表,我们除了生成实体类外,不需要生成其他接口和接口实现层,因为我们不单独调用它们。
对于具体业务对象对应的接口实现,我们除了确定它的继承关系外,我们还会重写它们的一些基类函数,从而实现更加精准的处理。
接口实现类的定义如下所示。
/// <summary> /// 应用层服务接口实现 /// </summary> public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService { }
一般情况下,我们至少需要在子类重写 CreateFilteredQueryAsync 和 ApplyDefaultSorting 两个函数,前者是条件的查询处理,后者是默认的排序处理操作。
/// <summary> /// 自定义条件处理 /// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<CustomerInfo> CreateFilteredQueryAsync(CustomerPagedDto input) { var query = base.CreateFilteredQueryAsync(input); query = query .WhereIF(!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIF(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals //年龄区间查询 .WhereIF(input.AgeStart.HasValue, s => s.Age >= input.AgeStart.Value) .WhereIF(input.AgeEnd.HasValue, s => s.Age <= input.AgeEnd.Value) //创建日期区间查询 .WhereIF(input.CreateTimeStart.HasValue, s => s.CreateTime >= input.CreateTimeStart.Value) .WhereIF(input.CreateTimeEnd.HasValue, s => s.CreateTime <= input.CreateTimeEnd.Value) ; return query; } /// <summary> /// 自定义排序处理 /// </summary> /// <param name="query">可查询LINQ</param> /// <returns></returns> protected override ISugarQueryable<CustomerInfo> ApplyDefaultSorting(ISugarQueryable<CustomerInfo> query) { return query.OrderBy(t => t.CreateTime, OrderByType.Desc); //如果先按第一个字段排序,然后再按第二字段排序,示例代码 //return base.ApplySorting(query, input).OrderBy(s=>s.Customer_ID).OrderBy(s => s.Seq); }
根据这些规则,编写我们所需的模板代码,让我们选择的数据库表名称、注释,以及表字段的名称、类型、注释,外键主键关系等信息为我们模板所用。
如下所示代码是NVelocity模板代码,用于生成上面的条件查询处理的,可以稍作了解。
/// <summary> /// 自定义条件处理 /// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<${ClassName}Info> CreateFilteredQueryAsync(${ClassName}PagedDto input) { var query = base.CreateFilteredQueryAsync(input); query = query #if(${PrimaryKeyNetType}=="string") .WhereIF(!input.ExcludeId.IsNullOrWhiteSpace(), t=>t.Id != input.ExcludeId) //不包含排除ID #else .WhereIF(input.ExcludeId.HasValue, t=>t.Id != input.ExcludeId) //不包含排除ID #end #foreach($EntityProperty in $EntityPropertyList) #if(${EntityProperty.ColumnInfo.IsForeignKey} || ${EntityProperty.PropertyName} == "Status" || ${EntityProperty.PropertyName} == "State" || ${EntityProperty.PropertyName} == "PID" || ${EntityProperty.PropertyName} == "Deleted") .WhereIF(#if(${EntityProperty.ColumnInfo.IsNumeric})input.${EntityProperty.PropertyName}.HasValue#else!input.${EntityProperty.PropertyName}.IsNullOrWhiteSpace()#end, s => s.${EntityProperty.PropertyName} == input.${EntityProperty.PropertyName}) #elseif(${EntityProperty.ColumnInfo.IsDateTime} || ${EntityProperty.ColumnInfo.IsNumeric}) //${EntityProperty.Description}区间查询 .WhereIF(input.${EntityProperty.PropertyName}Start.HasValue, s => s.${EntityProperty.PropertyName} >= input.${EntityProperty.PropertyName}Start.Value) .WhereIF(input.${EntityProperty.PropertyName}End.HasValue, s => s.${EntityProperty.PropertyName} <= input.${EntityProperty.PropertyName}End.Value) #elseif(${EntityProperty.ColumnInfo.NetType.Alias.ToLower()} != "byte[]" && ${EntityProperty.ColumnInfo.Name.Name.ToString()} != "AttachGUID") .WhereIF(#if(${EntityProperty.NetType.EndsWith("?")})input.${EntityProperty.PropertyName}.HasValue, t => t.${EntityProperty.PropertyName} == input.${EntityProperty.PropertyName}#else!input.${EntityProperty.PropertyName}.IsNullOrWhiteSpace(), t => t.${EntityProperty.PropertyName}.Contains(input.${EntityProperty.PropertyName})#end) //如需要精确匹配则用Equals #end ##endif #end #if(${HasCreationTime}) //创建日期区间查询 .WhereIF(input.CreationTimeStart.HasValue, s => s.CreationTime >= input.CreationTimeStart.Value) .WhereIF(input.CreationTimeEnd.HasValue, s => s.CreationTime <= input.CreationTimeEnd.Value) #else //创建日期区间查询(参考) //.WhereIF(input.CreationTimeStart.HasValue, s => s.CreationTime >= input.CreationTimeStart.Value) //.WhereIF(input.CreationTimeEnd.HasValue, s => s.CreationTime <= input.CreationTimeEnd.Value) #end; return query; }
当我们完成所需的模板代码开发后,就在代码生成工具主体界面中整合相关的生成功能菜单,界面效果如下所示。如需要下载测试代码生成工具Database2sharp,请到官网https://www.iqidi.com/database2sharp.htm 下载试用。
通过菜单选择【SqlSugar框架代码生成】,进一步选择数据库中的表进行生成,一步步处理即可,最后列出所选数据库表,并确认生成操作,即可生成SqlSugar框架核心项目的代码,如下图所示。
选择表进行生成后,生成的实体模型类如下所示,包括生成了中间表的实体类。
而接口实现则是根据具体的业务对象规则进行生成。
3、SqlSugar项目中Winform界面的生成
Winform界面包括普通列表/编辑界面处理,以及主从表界面处理两个部分,如下图所示。
生成的简单业务表界面,包括分页列表展示界面,在列表界面中整合查看、编辑、新增、删除、导入、导出、查询/高级查询等功能,整合的编辑界面也是依据数据库表的信息进行生成的。
列表界面和编辑界面效果如下所示。
而主从表界面生成的效果如下所示。
我们看看生成的Winform列表界面代码,如下所示。
另外我们把一些常用的处理逻辑放在函数中统一处理,如AddData、EditData、DeleteData、BindData、GetData、ImportData、ExportData等等,如下所示。
在获取数据的时候,我们根据用户的条件,构建一个分页查询对象传递,调用接口获得数据后,进行分页控件的绑定处理即可。
/// <summary> /// 获取数据 /// </summary> /// <returns></returns> private async Task<IPagedResult<CustomerInfo>> GetData() { CustomerPagedDto pagerDto = null; if (advanceCondition != null) { //如果有高级查询,那么根据输入信息构建查询条件 pagerDto = new CustomerPagedDto(this.winGridViewPager1.PagerInfo); pagerDto = dlg.GetPagedResult(pagerDto); } else { //构建分页的条件和查询条件 pagerDto = new CustomerPagedDto(this.winGridViewPager1.PagerInfo) { //添加所需条件 Name = this.txtName.Text.Trim(), }; //日期和数值范围定义 //年龄,需在CustomerPagedDto中添加 int? 类型字段AgeStart和AgeEnd var Age = new ValueRange<int?>(this.txtAge1.Text, this.txtAge2.Text); //数值类型 pagerDto.AgeStart = Age.Start; pagerDto.AgeEnd = Age.End; //创建时间,需在CustomerPagedDto中添加 DateTime? 类型字段CreationTimeStart和CreationTimeEnd var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型 pagerDto.CreateTimeStart = CreationTime.Start; pagerDto.CreateTimeEnd = CreationTime.End; } var result = await BLLFactory<CustomerService>.Instance.GetListAsync(pagerDto); return result; }
如果是高级查询,我们则是根据传入分页查询对象的属性在高级查询对话框中进行赋值,然后获得对象后进行查询获得记录的。
在代码生成工具中,我们根据实际项目的代码,定义好对应的模板文件,如下所示。
最后在生成代码的时候,整合这些NVelocity的模板文件,根据表对象的信息,生成对应的文件供我们开发使用即可。