2023年2月

在实际项目开发中,我们可能会碰到各种各样的项目环境,有些项目需要一个大而全的整体框架来支撑开发,有些中小项目这需要一些简单便捷的系统框架灵活开发。目前大型一点的框架,可以采用ABP或者ABP VNext的框架,两者整体思路和基础设计类似,不过ABP侧重于一个独立完整的项目框架,开发的时候统一整合处理;而ABP VNext则是以微服务架构为基础,各个模块独立开发,既可以整合在一个项目中,也可以以微服务进行单独发布,并统一通过网关处理进行交流。不管ABP或者ABP VNext框架,都集合了.NET CORE领域众多技术为一体,并且基础类设计上,错综复杂,关系较多,因此开发学习有一定的门槛,中小型项目应用起来有一定的费劲之处。本系列随笔介绍底层利用SqlSugar来做ORM数据访问模块,设计一个简单便捷一点的框架,本篇从基础开始介绍一些框架内容,参照一些ABP/ABP VNext中的一些类库处理,来承载类似条件分页信息,查询条件处理等处理细节。

1、基于SqlSugar开发框架的架构设计

主要的设计模块场景如下所示。

为了避免像ABP VNext框架那样分散几十个项目,我们尽可能聚合内容放在一个项目里面。

1)其中一些常用的类库,以及SqlSugar框架的基类放在框架公用模块里面。

2)Winform开发相关的基础界面以及通用组件内容,放在基础Winform界面库BaseUIDx项目中。

3)基础核心数据模块SugarProjectCore,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。其中Service里面一些框架基类和接口定义,统一也放在公用类库里面。

4)Winform应用模块,主要就是针对业务开发的WInform界面应用,而WInform开发为了方便,也会将一些基础组件和基类放在了BaseUIDx的Winform专用的界面库里面。

5)WebAPI项目采用基于.net Core6的项目开发,通过调用SugarProjectCore实现相关控制器API的发布,并整合Swagger发布接口,供其他前端界面应用进行调用。

6)纯前端通过API进行调用Web API的接口,纯前端模块可以包含Vue3&Element项目,以及基于EelectronJS应用,发布跨平台的基于浏览器的应用界面,以及其他App或者小程序整合Web API进行业务数据的处理或者展示需要。

如后端开发,我们可以在VS2022中进行管理,管理开发Winform项目、Web API项目等。

Winform界面,我们可以采用基于.net Framework开发或者.net core6进行开发均可,因为我们的SugarProjectCore项目是采用.net Standard模式开发,兼容两者。这里以权限模块来进行演示整合使用。

而纯前端的项目,我们可以基于VSCode或者 HBuilderX等工具进行项目的管理开发工作。

2、框架基础类的定义和处理

在开发一个易于使用的框架的时候,主要目的就是减少代码开发,并尽可能通过基类和泛型约束的方式,提高接口的通用性,并通过结合代码生成工具的方式,来提高标准项目的开发效率。

那么我们这里基于SqlSugar的ORM处理,来实现常规数据的增删改查等常规操作的时候,我们是如何进行这些接口的封装处理的呢。

例如,我们对于一个简单的客户信息表,如下所示。

那么它生成的SqlSugar实体类如下所示。

    /// <summary>
    ///客户信息///继承自Entity,拥有Id主键属性/// </summary>
    [SugarTable("T_Customer")]public class CustomerInfo : Entity<string>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicCustomerInfo()
{
this.CreateTime =System.DateTime.Now;
}
#region Property Members /// <summary> ///姓名/// </summary> public virtual string Name { get; set; }/// <summary> ///年龄/// </summary> public virtual int Age { get; set; }/// <summary> ///创建人/// </summary> public virtual string Creator { get; set; }/// <summary> ///创建时间/// </summary> public virtual DateTime CreateTime { get; set; }#endregion}

其中 Entity<string> 是我们根据需要定义一个基类实体对象,主要就是定义一个Id的属性来处理,毕竟对于一般表对象的处理,SqlSugar需要Id的主键定义(非中间表处理)。

[Serializable]public abstract class Entity<TPrimaryKey> : IEntity<TPrimaryKey>{/// <summary>
        ///实体类唯一主键/// </summary>
        [SqlSugar.SugarColumn(IsPrimaryKey = true, ColumnDescription = "主键")]public virtual TPrimaryKey Id { get; set; }
}

而IEntity<T>定义了一个接口

    public interface IEntity<TPrimaryKey>{/// <summary>
        ///实体类唯一主键/// </summary>
        TPrimaryKey Id { get; set; }
}

以上就是实体类的处理,我们一般为了查询信息,往往通过一些条件传入进行处理,那么我们就需要定义一个通用的分页查询对象,供我们精准进行条件的处理。

生成一个以***PageDto的对象类,如下所示。

    /// <summary>
    ///用于根据条件分页查询,DTO对象/// </summary>
    public classCustomerPagedDto : PagedAndSortedInputDto, IPagedAndSortedResultRequest
{
/// <summary> ///默认构造函数/// </summary> public CustomerPagedDto() : base() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skipCount">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public CustomerPagedDto(int skipCount, int resultCount) : base(skipCount, resultCount)
{
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> public CustomerPagedDto(PagerInfo pagerInfo) : base(pagerInfo)
{
}
#region Property Members /// <summary> ///不包含的对象的ID,用于在查询的时候排除对应记录/// </summary> public virtual string ExcludeId { get; set; }/// <summary> ///姓名/// </summary> public virtual string Name { get; set; }/// <summary> ///年龄-开始/// </summary> public virtual int? AgeStart { get; set; }/// <summary> ///年龄-结束/// </summary> public virtual int? AgeEnd { get; set; }/// <summary> ///创建时间-开始/// </summary> public DateTime? CreateTimeStart { get; set; }/// <summary> ///创建时间-结束/// </summary> public DateTime? CreateTimeEnd { get; set; }#endregion}

其中PagedAndSortedInputDto, IPagedAndSortedResultRequest都是参考来自于ABP/ABP VNext的处理方式,这样我们可以便于数据访问基类的查询处理操作。

接着我们定义一个基类MyCrudService,并传递如相关的泛型约束,如下所示

    /// <summary>
    ///基于SqlSugar的数据库访问操作的基类对象/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public abstract class MyCrudService<TEntity, TKey, TGetListInput>: 
IMyCrudService
<TEntity, TKey, TGetListInput> where TEntity : class, IEntity<TKey>, new()where TGetListInput : IPagedAndSortedResultRequest

我们先忽略基类接口的相关实现细节,我们看看对于这个
MyCrudService

IMyCrudService
我们应该如何使用的。

首先我们定义一个应用层的接口ICustomerService如下所示。

    /// <summary>
    ///客户信息服务接口/// </summary>
    public interface ICustomerService : IMyCrudService<CustomerInfo, string, CustomerPagedDto>, ITransientDependency
{

}

然后实现在CustomerService中实现它的接口。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService

这样我们对于特定Customer的接口在ICustomer中定义,标准接口直接调用基类即可。

基类MyCrudService提供重要的两个接口,让子类进行重写,以便于进行准确的条件处理和排序处理,如下代码所示。

    /// <summary>
    ///基于SqlSugar的数据库访问操作的基类对象/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public abstract class MyCrudService<TEntity, TKey, TGetListInput>: 
IMyCrudService
<TEntity, TKey, TGetListInput> where TEntity : class, IEntity<TKey>, new()whereTGetListInput : IPagedAndSortedResultRequest
{
/// <summary> ///留给子类实现过滤条件的处理/// </summary> /// <returns></returns> protected virtual ISugarQueryable<TEntity>CreateFilteredQueryAsync(TGetListInput input)
{
returnEntityDb.AsQueryable();
}
/// <summary> ///默认排序,通过ID进行排序/// </summary> /// <param name="query"></param> /// <returns></returns> protected virtual ISugarQueryable<TEntity> ApplyDefaultSorting(ISugarQueryable<TEntity>query)
{
if (typeof(TEntity).IsAssignableTo<IEntity<TKey>>())
{
return query.OrderBy(e =>e.Id);
}
else{return query.OrderBy("Id");
}
}
}

对于Customer特定的业务对象来说,我们需要实现具体的条件查询细节和排序条件,毕竟我们父类没有约束确定实体类有哪些属性的情况下,这些就交给子类做最合适了。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
/// <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)
;
returnquery;
}
/// <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); }
}

通过 CreateFilteredQueryAsync 的精确条件处理,我们就可以明确实体类的查询条件处理,因此对于CustomerPagedDto来说,就是可以有客户端传入,服务后端的基类进行处理了。

如基类的分页条件查询函数GetListAsync就是根据这个来处理的,它的实现代码如下所示。

        /// <summary>
        ///根据条件获取列表/// </summary>
        /// <param name="input">分页查询条件</param>
        /// <returns></returns>
        public virtual async Task<PagedResultDto<TEntity>>GetListAsync(TGetListInput input)
{
var query =CreateFilteredQueryAsync(input);var totalCount = awaitquery.CountAsync();

query
=ApplySorting(query, input);
query
=ApplyPaging(query, input);var list = awaitquery.ToListAsync();return new PagedResultDto<TEntity>(
totalCount,
list
);
}

而其中 ApplySorting 就是根据条件决定是否选择子类实现的默认排序进行处理的。

        /// <summary>
        ///记录排序处理/// </summary>
        /// <returns></returns>
        protected virtual ISugarQueryable<TEntity> ApplySorting(ISugarQueryable<TEntity>query, TGetListInput input)
{
//Try to sort query if available if (input isISortedResultRequest sortInput)
{
if (!sortInput.Sorting.IsNullOrWhiteSpace())
{
returnquery.OrderBy(sortInput.Sorting);
}
}
//IQueryable.Task requires sorting, so we should sort if Take will be used. if (input isILimitedResultRequest)
{
returnApplyDefaultSorting(query);
}
//No sorting returnquery;
}

对于获取单一对象,我们一般提供一个ID主键获取即可。

        /// <summary>
        ///根据ID获取单一对象/// </summary>
        /// <param name="id">主键ID</param>
        /// <returns></returns>
        public virtual async Task<TEntity>GetAsync(TKey id)
{
return awaitEntityDb.GetByIdAsync(id);
}

也可以根据用户的Express条件进行处理,在基类我们定义很多这样的Express条件处理,便于子类进行条件处理的调用。如对于删除,可以指定ID,也可以指定条件删除。

        /// <summary>
        ///删除指定ID的对象/// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool>DeleteAsync(TKey id)
{
return awaitEntityDb.DeleteByIdAsync(id);
}
/// <summary>
        ///根据指定条件,删除集合/// </summary>
        /// <param name="input">表达式条件</param>
        /// <returns></returns>
        public virtual async Task<bool> DeleteAsync(Expression<Func<TEntity, bool>>input)
{
var result = awaitEntityDb.DeleteAsync(input);returnresult;
}

如判断是否存在也是一样处理

        /// <summary>
        ///判断是否存在指定条件的记录/// </summary>
        /// <param name="id">ID 主键</param>
        /// <returns></returns>
        public virtual async Task<bool>IsExistAsync(TKey id)
{
var info = awaitEntityDb.GetByIdAsync(id);var result = (info != null);returnresult;
}
/// <summary> ///判断是否存在指定条件的记录/// </summary> /// <param name="input">表达式条件</param> /// <returns></returns> public virtual async Task<bool> IsExistAsync(Expression<Func<TEntity, bool>>input)
{
var result = awaitEntityDb.IsAnyAsync(input);returnresult;
}

关于Web API的处理,我在随笔《
基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用
》中也有介绍,主要就是先弄好.net6的开发环境,然后在进行相关的项目开发即可。

根据项目的需要,我们定义了一些控制器的基类,用于实现不同的功能。

其中ControllerBase是.net core Web API中的标准控制器基类,我们由此派生一个LoginController用于登录授权,而BaseApiController则处理常规接口用户身份信息,而BusinessController则是对标准的增删改查等基础接口进行的封装,我们实际开发的时候,只需要开发编写类似CustomerController基类即可。

BaseApiController没有什么好介绍的,就是封装一下获取用户的身份信息。

可以通过下面代码获取接口用户的Id

        /// <summary>
        ///当前用户身份ID/// </summary>
        protected virtual string? CurrentUserId => HttpContext.User.FindFirst(JwtClaimTypes.Id)?.Value;

而BusinessController控制器则是继承这个BaseApiController即可。通过泛型约束传入相关的对象信息。

    /// <summary>
    ///本控制器基类专门为访问数据业务对象而设的基类/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    [Route("[controller]")]
[Authorize]
//需要授权登录访问 public class BusinessController<TEntity, TKey, TGetListInput> : BaseApiController where TEntity : class, IEntity<TKey>, new()whereTGetListInput : IPagedAndSortedResultRequest
{
/// <summary> ///通用基础操作接口/// </summary> protected IMyCrudService<TEntity, TKey, TGetListInput> _service { get; set; }/// <summary> ///构造函数,初始化基础接口/// </summary> /// <param name="service">通用基础操作接口</param> public BusinessController(IMyCrudService<TEntity, TKey, TGetListInput>service)
{
this._service =service;
}

....

这个基类接收一个符合基类接口定义的对象作为基类增删删改查等处理方法的接口对象。在具体的CustomerController中的定义处理如下所示。

    /// <summary>
    ///客户信息的控制器对象/// </summary>
    public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto>
{privateICustomerService _customerService;/// <summary>
        ///构造函数,并注入基础接口对象/// </summary>
        /// <param name="customerService"></param>
        public CustomerController(ICustomerService customerService) :base(customerService)
{
this._customerService =customerService;
}
}

这样就可以实现基础的相关操作了。如果需要特殊的接口实现,那么定义方法实现即可。

类似字典项目中的控制器处理代码如下所示。定义好HTTP方法,路由信息等即可。

        /// <summary>
        ///根据字典类型ID获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
[HttpGet]
[Route(
"by-typeid/{dictTypeId}")]public async Task<Dictionary<string, string>> GetDictByTypeID(stringdictTypeId)
{
return await_dictDataService.GetDictByTypeID(dictTypeId);
}
/// <summary> ///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary> /// <param name="dictTypeName">字典类型名称</param> /// <returns></returns> [HttpGet]
[Route(
"by-typename/{dictTypeName}")]public async Task<Dictionary<string, string>> GetDictByDictType(stringdictTypeName)
{
return await_dictDataService.GetDictByDictType(dictTypeName);
}

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在前面介绍的SqlSugar的相关查询处理操作中,我们主要以单表的方式生成相关的实体类,并在查询的时候,对单表的字段进行条件的对比处理,从而返回对应的数据记录。本篇随笔介绍在一些外键或者中间表的处理中,如何遍历查询并获得所需的记录操作。

1、回顾单表的操作查询

我在《
基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用
》中介绍过的Customer表信息,就是一个单表的处理。

例如,我们对于一个简单的客户信息表,如下所示。

生成对应的实体对象CustomerInfo外,同时生成 CustomerPagedDto  的分页查询条件对象。

在继承基类后

/// <summary>
///应用层服务接口实现/// </summary>
public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
....
}

并重写 CreateFilteredQueryAsync 函数,从而实现了条件的精确查询处理。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
/// <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)
;
returnquery;
}

在表的对应实体信息没有其他表关联的时候,我们直接通过SqlSugar的基础接口返回对象列表即可。

通过 CreateFilteredQueryAsync 的精确条件处理,我们就可以明确实体类的查询条件处理,因此对于CustomerPagedDto来说,就是可以有客户端传入,服务后端的基类进行处理了。

如基类的分页条件查询函数GetListAsync就是根据这个来处理的,它的实现代码如下所示。

        /// <summary>
        ///根据条件获取列表/// </summary>
        /// <param name="input">分页查询条件</param>
        /// <returns></returns>
        public virtual async Task<PagedResultDto<TEntity>>GetListAsync(TGetListInput input)
{
var query =CreateFilteredQueryAsync(input);var totalCount = awaitquery.CountAsync();

query
=ApplySorting(query, input);
query
=ApplyPaging(query, input);var list = awaitquery.ToListAsync();return new PagedResultDto<TEntity>(
totalCount,
list
);
}

也就是说只要继承了 CustomerService ,我们默认调用基类的 GetListAsync 就可以返回对应的列表记录了。

如在Web API的控制器中调用获取记录返回,调用处理的代码如下所示。

        /// <summary>
        ///获取所有记录/// </summary>
[HttpGet]
[Route(
"all")]
[HttpGet]
public virtual async Task<ListResultDto<TEntity>>GetAllAsync()
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey);return await_service.GetAllAsync();
}

而对于Winform的调用,我们这里首先利用代码生成工具生成对应的界面和代码

查看其调用的界面代码

而其中GetData中的函数部分内容如下所示。

        /// <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);returnresult;
}

列表界面效果如下所示。

2、基于中间表的查询处理

前面的查询处理,主要就是针对没有任何关系的表实体对象的返回处理,但往往我们开发的时候,会涉及到很多相关的表,单独的表相对来说还是比较少,因此对表的关系遍历处理和中间表的关系转换,就需要在数据操作的时候考虑的了。

例如对于字典大类和字典项目的关系,如下所示。

以及在权限管理系统模块中,用户、角色、机构、权限等存在着很多中间表的关系,如下所示。

如对于字典表关系处理,我们采用Queryable<DictDataInfo, DictTypeInfo>的查询处理方式,可以联合两个表对象实体进行联合查询,如下代码所示。

        /// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeName">字典类型名称</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByDictType(stringdictTypeName)
{
var query = this.Client.Queryable<DictDataInfo, DictTypeInfo>(
(d, t)
=> d.DictType_ID == t.Id && t.Name ==dictTypeName)
.Select(d
=> d); //联合条件获取对象 query= query.OrderBy(d => d.DictType_ID).OrderBy(d => d.Seq);//排序 var list = await query.ToListAsync();//获取列表 var dict = new Dictionary<string, string>();foreach (var info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Value);
}
}
returndict;
}

其中的Client对象是DbContext对象实例的Client属性,如下图所示。

这个对象是在DbContext对象中构建的,如下所示。

            this.Client = new SqlSugarScope(newConnectionConfig()
{
DbType
= this.DbType,
ConnectionString
= this.ConnectionString,
InitKeyType
=InitKeyType.Attribute,
IsAutoCloseConnection
= true, //是否自动关闭连接 AopEvents = newAopEvents
{
OnLogExecuting
= (sql, p) =>{//Log.Information(sql);//Log.Information(string.Join(",", p?.Select(it => it.ParameterName + ":" + it.Value))); }
}
});

我们查看Queryable,可以看到这个SqlSugar基类函数 Queryable 提供了很多重载函数,也就是它们可以提供更多的表对象进行联合查询的,如下所示。

前面介绍的是外键的一对多的关系查询,通过两个对象之间进行的关系连接,从而实现另一个对象属性的对比查询操作的。

对于中间表的处理,也是类似的情况,我们通过对比中间表的属性,从而实现条件的过滤处理。如下是对于角色中相关关系的中间表查询。

        /// <summary>
        ///根据用户ID获取对应的角色列表/// </summary>
        /// <param name="userID">用户ID</param>
        /// <returns></returns>
        private async Task<List<RoleInfo>> GetByUser(intuserID)
{
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();//获取列表 returnlist;
}
/// <summary> ///根据机构获取对应的角色列表(判断机构角色中间表)/// </summary> /// <param name="ouID">机构的ID</param> /// <returns></returns> public async Task<List<RoleInfo>> GetRolesByOu(intouID)
{
var query = this.Client.Queryable<RoleInfo, OU_RoleInfo>(
(t, m)
=> t.Id == m.Role_ID && m.Ou_ID ==ouID)
.Select(t
=> t); //联合条件获取对象 query= query.OrderBy(t => t.CreateTime);//排序 var list = await query.ToListAsync();//获取列表 returnlist;
}

通过联合查询中间表对象信息,可以对它的字段属性进行条件联合,从而获得所需的记录。

这里User_RoleInfo和Ou_RoleInfo表也是根据中间表的属性生成的,不过它们在业务层并没有任何关联操作,也不需要生成对应的Service层,因此只需要生成相关的Model类实体即可。

    /// <summary>
    ///用户角色关联/// </summary>
    [SugarTable("T_ACL_User_Role")]public classUser_RoleInfo
{
/// <summary> ///用户ID/// </summary> [Required]public virtual int User_ID { get; set; }/// <summary> ///角色ID/// </summary> [Required]public virtual int Role_ID { get; set; }

}
    /// <summary>
    ///机构角色关联/// </summary>
    [SugarTable("T_ACL_OU_Role")]public classOU_RoleInfo 
{
/// <summary> ///机构ID/// </summary> [Required]public virtual int Ou_ID { get; set; }/// <summary> ///角色ID/// </summary> [Required]public virtual int Role_ID { get; set; }
}

可以看到这两个实体不同于其他实体,它们没有基类继承关系,而一般标准的实体是有的。

    /// <summary>
    ///角色信息/// </summary>
    [SugarTable("T_ACL_Role")]public class RoleInfo : Entity<int> {  }


    /// <summary>
    ///功能菜单/// </summary>
    [SugarTable("T_ACL_Menu")]public class MenuInfo : Entity<string> { }

所以我们就不需要构建它们的Service层来处理数据,它的存在合理性只是在于能够和其他实体对象进行表的联合查询处理而且。

最后贴上一个整合SqlSugar处理而完成的系统基础框架的Winform端界面,其中包括用户、组织机构、角色管理、权限管理、菜单管理、日志、字典、客户信息等业务表的处理。

以证所言非虚。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

我喜欢在一个项目开发模式成熟的时候,使用代码生成工具Database2Sharp来配套相关的代码生成,对于我介绍的基于SqlSugar的开发框架,从整体架构确定下来后,我就着手为它们量身定做相关的代码开发,这样可以在后续整合项目功能的时候,利用代码生成工具快速的生成所需要模块的骨架代码,然后在这个基础上逐渐增加自定义的内容即可,方便快捷。本篇随笔介绍基于SqlSugar的开发框架,对框架中涉及到的各个分层或者模块代码进行生成的处理。

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 classUser_RoleInfo
{
}

只需要简单的标注好SugarTable属性,让他可以和其他业务表进行关联查询即可。

        /// <summary>
        ///根据用户ID获取对应的角色列表/// </summary>
        /// <param name="userID">用户ID</param>
        /// <returns></returns>
        private async Task<List<RoleInfo>> GetByUser(intuserID)
{
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();//获取列表 returnlist;
}

对于接口层的类,我们只需要按固定的继承关系处理好,以及类的名称变化即可。

    /// <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)
;
returnquery;
}
/// <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;returnquery;
}

当我们完成所需的模板代码开发后,就在代码生成工具主体界面中整合相关的生成功能菜单,界面效果如下所示。如需要下载测试代码生成工具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);returnresult;
}

如果是高级查询,我们则是根据传入分页查询对象的属性在高级查询对话框中进行赋值,然后获得对象后进行查询获得记录的。

在代码生成工具中,我们根据实际项目的代码,定义好对应的模板文件,如下所示。

最后在生成代码的时候,整合这些NVelocity的模板文件,根据表对象的信息,生成对应的文件供我们开发使用即可。

            #region Model 实体部分

            string entityTemplateFile = ProjectPath + "Templates/Entity.cs.vm";var entityAdapter = newSugarEntityAdapter(databaseInfo, selectedTableNames, entityTemplateFile);
entityAdapter.DirectoryOfOutput
= mainSetting.RootNameSpace + "/Core/Modal";
entityAdapter.Execute();
#endregion #region Interface部分和Application部分 var appInterface = new SugarServiceInterfaceAdapter(databaseInfo, selectedTableNames, ProjectPath + "Templates/IService.cs.vm", databaseTypeName);
appInterface.DirectoryOfOutput
= mainSetting.RootNameSpace + "/Core/Interface";
appInterface.Execute();
var appService = new SugarServiceAdapter(databaseInfo, selectedTableNames, ProjectPath + "Templates/Service.cs.vm", databaseTypeName);
appService.DirectoryOfOutput
= mainSetting.RootNameSpace + "/Core/Service";
appService.Execute();
#endregion #region Web API Controller 部分 var controller = new SugarControllerAdapter(databaseInfo, selectedTableNames, ProjectPath + "Templates/Controller.cs.vm", databaseTypeName);
controller.DirectoryOfOutput
= mainSetting.RootNameSpace + "/Controller";
controller.Execute();
#endregion

如需要下载测试代码生成工具
Database2sharp
,请到官网
https://www.iqidi.com/database2sharp.htm
下载试用。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

我们在设计数据库表的时候,往往为了方便,主键ID一般采用字符串类型或者GUID类型,这样对于数据库表记录的迁移非常方便,而且有时候可以在处理关联记录的时候,提前对应的ID值。但有时候进行数据记录插入的时候,往往忽略了对ID的赋值处理。为了便于使用或者允许自动赋值,我们可以在数据访问基类中对GUID主键进行自动赋值处理。

1、实体类主键属性的处理

在我们设计基于SqlSugar的框架的时候,实体类定义一个基类Entity<T>,如下代码所示。

[Serializable]public abstract class Entity<TPrimaryKey> : IEntity<TPrimaryKey>{/// <summary>
        ///实体类唯一主键/// </summary>
        [SqlSugar.SugarColumn(IsPrimaryKey = true, ColumnDescription = "主键")]public virtual TPrimaryKey Id { get; set; }

一般可以扩展字符串,整形等等类型的实体类。

默认的Entity定义为整形的,如下所示。自增长的整形主键,不需要插入值,它在记录写入的时候获得对应的Id值。

[Serializable]public abstract class Entity : Entity<int>, IEntity
{
/// <summary> ///ID 主键,自增长类型/// </summary> [SqlSugar.SugarColumn(IsPrimaryKey = true, IsIdentity = true)]public override int Id { get; set; }
}

对于字符型类型的ID键,可以在构造函数中对ID进行初始化。

    /// <summary>
    ///客户信息///继承自Entity,拥有Id主键属性/// </summary>
    [SugarTable("T_Customer")]public class CustomerInfo : Entity<string>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicCustomerInfo()
{
this.Id =System.Guid.NewGuid().ToString();this.CreateTime =System.DateTime.Now;

}

或者我们在数据插入一条新记录的时候,判断主键是否为空,然后赋值给它,或者唯一的GUID值。

使用Guid.NewGuid() 的处理,这样好处就是可以获得一个唯一的GUID值,而弊端是ID是无序的,没有先后顺序,对ID排序就是无意义了。

为了解决这个问题,我们还是引入Abp VNext的规则,生成一个有序的GUID值,同时在数据库访问基类,对插入记录、更新记录的时候,判断ID(对GUID类型或者字符串类型的主键ID)是否为空,为空则赋值一个有序的GUID给它,则可以完美解决问题了。

这样我们定义实体类的时候,ID值可以不初始化,让它保留位空,可以让用户主动设置值或者自动基类处理赋值。

    /// <summary>
    ///客户信息///继承自Entity,拥有Id主键属性/// </summary>
    [SugarTable("T_Customer")]public class CustomerInfo : Entity<string>{/// <summary>
        ///默认构造函数(需要初始化属性的在此处理)/// </summary>
        publicCustomerInfo()
{
this.CreateTime =System.DateTime.Now;
}

2、基类判断ID是否为空并赋值

对于GUID或者字符串类型的ID值,为什么设置有序GUID,可以参考链接了解下:
https://github.com/abpframework/abp/blob/48c52625f4c4df007f04d5ac6368b07411aa7521/docs/zh-Hans/Guid-Generation.md

一般情况下,我们利用SqlSugar插入一个新记录的时候,是如下代码

        /// <summary>
        ///创建对象/// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual async Task<bool>InsertAsync(TEntity input)
{
return awaitEntityDb.InsertAsync(input);
}

而为了判断Id是否为空,我们需要对ID类型进行判断,判断是否字符串类型或者GUID类型,如果为空则自动赋值它,因此我们在插入前进行一个判断处理,如下代码所示。

        /// <summary>
        ///创建对象/// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual async Task<bool>InsertAsync(TEntity input)
{
SetIdForGuids(input);
//如果Id为空,设置有序的GUID值 return awaitEntityDb.InsertAsync(input);
}

其中SetIdForGuids是获得有序GUID的值的函数。

        /// <summary>
        ///为新创建的实体对象,设置主键Id的值为有序的GUID值(GUID类型或者字符串类型试用)/// </summary>
        public virtual voidSetIdForGuids(TEntity entity)
{
if (entity is IEntity<Guid> entityWithGuidId && entityWithGuidId.Id ==Guid.Empty)
{
//默认的GUID类型 var guidType =SequentialGuidType.SequentialAsString;switch(this.dbContext.DbType) //根据不同的数据库类型获取合适的生成序列方式 {caseSqlSugar.DbType.SqlServer:
guidType
=SequentialGuidType.SequentialAtEnd;break;caseSqlSugar.DbType.MySql:caseSqlSugar.DbType.PostgreSQL:
guidType
=SequentialGuidType.SequentialAsString;break;caseSqlSugar.DbType.Oracle:
guidType
=SequentialGuidType.SequentialAsBinary;break;
}
var guid =GetSequentialGuid(guidType);
entityWithGuidId.Id
=guid;
}
else if (entity is IEntity<string> entityWithStringId && string.IsNullOrWhiteSpace(entityWithStringId.Id))
{
var guid =GetSequentialGuid(SequentialGuidType.SequentialAsString);
entityWithStringId.Id
=guid.ToString();
}
}

根据不同的数据库特性类型,构建不同的GUID值,如果是字符串的Id,我们统一采用 SequentialAsString 这个方式,这个也是支持字符串的常规排序处理,这样我们既获得了一个不重复的GUID值,也可以对ID进行排序,它是根据先后顺序排序的。

        /// <summary>
        ///获取可以生成连续的GUID/// </summary>
        /// <returns></returns>
        protectedGuid GetSequentialGuid(SequentialGuidType sequentialGuidType)
{
//使用指定序列创建的(生成连续的GUID)//参考链接了解细节:(https://github.com/abpframework/abp/blob/48c52625f4c4df007f04d5ac6368b07411aa7521/docs/zh-Hans/Guid-Generation.md) var options = newAbpSequentialGuidGeneratorOptions()
{
DefaultSequentialGuidType
=sequentialGuidType//SequentialAtEnd(default) 用于SQL Server.//SequentialAsString 用于MySQL和PostgreSQL.//SequentialAsBinary 用于Oracle. };return newSequentialGuidGenerator(options).Create();
}

添加几个字典类型(字符串ID)的记录进行测试。

可以看到ID的类型前缀部分是一样的,后面变化,以ID正序排序,是根据写入时间顺序处理的。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在前面随笔,我们介绍过这个基于SqlSugar的开发框架,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。在Service层中,往往除了本身的一些增删改查等处理操作外,也需要涉及到相关业务的服务接口,这些服务接口我们通过利用.net 的接口注入方式,实现IOC控制反转的处理的。

1、框架Service层的模块

如下面的VS中的项目服务层,包含很多业务表的服务接口实现,如下所示。

我们以其中简单的Customer业务表为例,它的服务类代码如下所示(主要关注服务类的定义即可)。

    /// <summary>
    ///客户信息应用层服务接口实现/// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
...............
}

它除了在泛型约束中增加SqlSugar实体类,主键类型,分页条件对象外,还继承接口 ICustomerService ,这个接口就是我们实现IOC的第一步,服务层继承指定的接口实现,对我们实现IOC控制反转提供便利。

    /// <summary>
    ///客户信息服务接口/// </summary>
    public interface ICustomerService : IMyCrudService<CustomerInfo, string, CustomerPagedDto>, ITransientDependency
{

}

这个客户信息业务处理,是比较典型的单表处理案例,它没有涉及到相关服务接口的整合,如果我们在其中服务接口中需要调用其他服务接口,那么我们就需要通过构造函数注入接口对象的方式获得对象的实例,如下我们说介绍的就是服务调用其他相关接口的实现。

2、服务层的接口注入

如对于角色服务接口来说,它往往和用户、机构有关系,因此我们在角色的服务接口层,可以整合用户、机构的对应服务接口,如下代码所示。

    /// <summary>
    ///角色信息 应用层服务接口实现/// </summary>
    public class RoleService : MyCrudService<RoleInfo,int, RolePagedDto>, IRoleService
{
privateIOuService _ouService;privateIUserService _userService;/// <summary> ///默认构造函数/// </summary> /// <param name="ouService">机构服务接口</param> /// <param name="userService">用户服务接口</param> publicRoleService(IOuService ouService, IUserService userService)
{
this._ouService =ouService;this._userService =userService;
}

}

通过构造函数的注入,我们就可以获得对应接口实现的实例,进行调用它的服务层方法使用了。

这样我们在角色的服务接口实现中,就可以调用其他如用户、机构相关的服务接口了。

其他模块的处理方式也是类似,如字典项目中,使用字典类型的服务接口。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class DictDataService : MyCrudService<DictDataInfo, string, DictDataPagedDto>, IDictDataService
{
/// <summary> ///测试字典类型接口/// </summary> protectedIDictTypeService _dictTypeService;/// <summary> ///注入方式获取接口/// </summary> /// <param name="dictTypeService">字典类型处理</param> publicDictDataService(IDictTypeService dictTypeService)
{
this._dictTypeService =dictTypeService;
}
}

这里值得注意的是,由于接口层是同级对象,因此要避免接口的相互引用而导致出错,依赖关系要清晰,才不会发生这个情况。

3、服务接口的实例的容器注册

在服务层中,我们是通过参数化构造函数的方式,引入对应的接口的,这个操作方式是构造函数的注入处理。

不过在此之前,我们需要在.net 的内置IOC容器中注册对应的接口实例,否则参数化构造函数会因为找不到接口实例而出错。

.net 的内置Ioc容器及注册处理,我们需要在nuget引入下面两个引用。

1、Microsoft.Extensions.DependencyInjection2、Microsoft.Extensions.DependencyInjection.Abstractions

.net 中 负责依赖注入和控制反转的核心组件有两个:IServiceCollection和IServiceProvider。其中,IServiceCollection负责注册,IServiceProvider负责提供实例。

在注册接口和类时,
IServiceCollection
提供了三种注册方法,如下所示:

1、services.AddTransient<IDictDataService, DictDataService>();  // 瞬时生命周期
2、services.AddScoped
<IDictDataService, DictDataService>(); // 域生命周期
3、services.AddSingleton
<IDictDataService, DictDataService>(); // 全局单例生命周期

如果使用
AddTransient
方法注册,
IServiceProvider
每次都会通过
GetService
方法创建一个新的实例;

如果使用
AddScoped
方法注册, 在同一个域(
Scope
)内,
IServiceProvider
每次都会通过
GetService
方法调用同一个实例,可以理解为在局部实现了单例模式;

如果使用
AddSingleton
方法注册, 在整个应用程序生命周期内,
IServiceProvider
只会创建一个实例。

我们为了在注册的时候方便通过遍历方式处理接口实例的注册,因此我们根据这几种关系定义了几个基类接口,便于根据特定的接口方式来构建接口实例。

namespaceWHC.Framework.ControlUtil
{
//用于定义这三种生命周期的标识接口 /// <summary> ///三种标识接口的基类接口/// </summary> public interfaceIDependency
{
}
/// <summary> ///瞬时(每次都重新实例)/// </summary> public interfaceITransientDependency : IDependency
{
}
/// <summary> ///单例(全局唯一)/// </summary> public interfaceISingletonDependency : IDependency
{
}
/// <summary> ///一个请求内唯一(线程内唯一)/// </summary> public interfaceIScopedDependency : IDependency
{
}
}

这样我们在定义注册类型的时候,通过它的接口指定属于上面那种类型。如对于字典项目的服务层,我们约定采用瞬时的注册方式,那么它的接口定义如下所示。

    /// <summary>
    ///字典项目服务接口/// </summary>
    public interface IDictDataService : IMyCrudService<DictDataInfo, string, DictDataPagedDto>, ITransientDependency
{
}

配置自动注册接口的时候,我们添加如下函数处理即可。

        /// <summary>
        ///配置依赖注入对象/// </summary>
        /// <param name="services"></param>
        public static voidConfigureRepository(IServiceCollection services)
{
#region 自动注入对应的服务接口 //services.AddSingleton<IDictDataService, DictDataService>();//services.AddScoped<IUserService, UserService>(); var baseType = typeof(IDependency);var path = AppDomain.CurrentDomain.RelativeSearchPath ??AppDomain.CurrentDomain.BaseDirectory;var getFiles = Directory.GetFiles(path, "*.dll").Where(Match); //.Where(o=>o.Match()) var referencedAssemblies = getFiles.Select(Assembly.LoadFrom).ToList(); //.Select(o=> Assembly.LoadFrom(o)) var ss = referencedAssemblies.SelectMany(o =>o.GetTypes());var types =referencedAssemblies
.SelectMany(a
=>a.DefinedTypes)
.Select(type
=>type.AsType())
.Where(x
=> x != baseType &&baseType.IsAssignableFrom(x)).ToList();var implementTypes = types.Where(x =>x.IsClass).ToList();var interfaceTypes = types.Where(x =>x.IsInterface).ToList();foreach (var implementType inimplementTypes)
{
if (typeof(IScopedDependency).IsAssignableFrom(implementType))
{
var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddScoped(interfaceType, implementType);
}
else if (typeof(ISingletonDependency).IsAssignableFrom(implementType))
{
var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddSingleton(interfaceType, implementType);
}
else{var interfaceType = interfaceTypes.FirstOrDefault(x =>x.IsAssignableFrom(implementType));if (interfaceType != null)
services.AddTransient(interfaceType, implementType);
}
}
#endregion}

上面根据我们自定义接口的不同,适当的采用不同的注册方式来加入Ioc容器中,从而实现了接口的注册,在服务层中就可以通过构造函数注入的方式获得对应的接口实例了。

这样,不管是在WInform的启动模块中,还是在Web API的启动模块中,我们在IOC容器中加入对应的接口即可,如下所示。

/// <summary>
///应用程序的主入口点。/// </summary>
[STAThread]static voidMain()
{
//IServiceCollection负责注册 IServiceCollection services = newServiceCollection();//services.AddSingleton<IDictDataService, DictDataService>();//services.AddSingleton<IDictTypeService, DictTypeService>();//添加IApiUserSession实现类 services.AddSingleton<IApiUserSession, ApiUserPrincipal>();//调用自定义的服务注册 ServiceInjection.ConfigureRepository(services);//IServiceProvider负责提供实例 IServiceProvider provider =services.BuildServiceProvider();
services.AddSingleton(provider);
//注册到服务集合中,需要可以在Service中构造函数中注入使用

Web API中的代码如下所示

//添加HTTP上下文访问
builder.Services.AddHttpContextAccessor();//配置依赖注入访问数据库
ServiceInjection.ConfigureRepository(builder.Services);//添加IApiUserSession实现类
builder.Services.AddSingleton<IApiUserSession, ApiUserPrincipal>();var app = builder.Build();

都是类似的处理方式。

同样在Web API项目中的控制器处理中,也是一样通过构造函数注入的方式使用接口的,如下所示。

namespaceWebApi.Controllers
{
/// <summary> ///客户信息的控制器对象/// </summary> public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto> {privateICustomerService _customerService;/// <summary> ///构造函数,并注入基础接口对象/// </summary> /// <param name="customerService"></param> public CustomerController(ICustomerService customerService) :base(customerService)
{
this._customerService =customerService;
}
}
}

或者登录处理的控制器定义如下。

    /// <summary>
    ///登录获取令牌授权的处理/// </summary>
    [Route("api/[controller]")]
[ApiController]
public classLoginController : ControllerBase
{
private readonlyIHttpContextAccessor _contextAccessor;private readonlyIConfiguration _configuration;private readonlyIUserService _userService;/// <summary> ///令牌失效天数,默认令牌7天有效期/// </summary> protected const int expiredDays = 7;/// <summary> ///构造函数,注入所需接口/// </summary> /// <param name="configuration">配置对象</param> /// <param name="httpContext">HTTP上下文对象</param> /// <param name="userService">用户信息</param> publicLoginController(IConfiguration configuration, IHttpContextAccessor httpContext,
IUserService userService
)
{
this._configuration =configuration;this._contextAccessor =httpContext;this._userService =userService;
}

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面