使用可查询扩展取回外键数据值

Posted

技术标签:

【中文标题】使用可查询扩展取回外键数据值【英文标题】:Using Queryable Extension to get back Foreign Key Data Value 【发布时间】:2022-01-12 17:23:17 【问题描述】:

我有我的计量单位,用户填写并保存,然后他们可以保存一个单位尺寸列表,该列表有自己的表格并且是计量单位的外键。当我取回所有数据时,Unit Size 值返回空白。

我已经阅读了六种方法来做到这一点,但我不理解它们。对我来说最有意义的一个是使用 Queryable 扩展,所以我试图走这条路,但我的代码还没有完全到达那里。

这是我所在的位置 - 这些是我的实体:

namespace Mudman.Data.Entities
   
    [Table("UnitOfMeasure")]
    public class UnitOfMeasure : IEntityBase, IAuditBase
    
        [Key]
        [Column("UnitOfMeasureId")]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public string Id  get; set; 

        [Required]
        [ForeignKey("TenantId")]
        public string TenantId  get; set; 

        [JsonIgnore]
        public virtual Tenant Tenant  get; set; 
        public string Name  get; set; 

        public virtual IEnumerable<UnitOfMeasureSize> UnitSize  get; set; 

        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public DateTime CreateDate  get; set;  = DateTime.UtcNow;

        [StringLength(255)]
        public string CreateUserId  get; set; 

        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public DateTime UpdateDate  get; set; 

        [StringLength(255)]
        public string UpdateUserId  get; set; 
    

计量单位尺寸实体:

namespace Mudman.Data.Entities

    [Table("UnitOfMeasureSize")]
    public class UnitOfMeasureSize : IEntityBase, IAuditBase
    
        [Key]
        [Column("UnitOfMeasureSize")]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public string Id  get; set; 

        [Required]
        [ForeignKey("TenantId")]
        public string TenantId  get; set; 

        [JsonIgnore]
        public virtual Tenant Tenant  get; set; 

        [Required]
        [ForeignKey("UnitOfMeasureId")]
        public string UnitOfMeasureId  get; set; 
        public virtual UnitOfMeasure UnitOfMeasure  get; set; 

        [Required]
        public int UnitSize  get; set; 

        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public DateTime CreateDate  get; set;  = DateTime.UtcNow;

        [StringLength(255)]
        public string CreateUserId  get; set; 

        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public DateTime UpdateDate  get; set; 

        [StringLength(255)]
        public string UpdateUserId  get; set; 
    

包括单位大小的计量单位存储库:

namespace Mudman.Repository

   public class UnitOfMeasureRepository : EntityBaseRepository<UnitOfMeasure>, 
   IUnitOfMeasureRepository
   
       MudmanDbContext context;

       public UnitOfMeasureRepository(MudmanDbContext context) : base(context)
       
             this.context = context; ;
       

       public  IEnumerable<UnitOfMeasure> GetAllUnitsOfMeasure(string TenantId)
       
            var result =  context.UnitOfMeasure
                                 .Where( uom => uom.TenantId == TenantId)
                                 .Include(uom => uom.UnitSize);

            return result;
        
    

我服务中的GetAllAsync 方法:

 public  Task<IEnumerable<UnitOfMeasureViewModel>> GetAllAsync()
     
     var result =  _unitOfMeasureRepo.GetAllUnitsOfMeasure(TenantId);
     result.OrderBy(r => r.Name);
     
     return _mapper.Map<List<UnitOfMeasure>, List<UnitOfMeasureViewModel>>(result.ToList());
 

AutoMapper 代码:

    CreateMap<UnitOfMeasure, UnitOfMeasureViewModel>().ReverseMap()
        .ForMember(dest => dest.UnitSize, uos => uos.Ignore())
        .ForMember(uom => uom.UnitSize, src => src.MapFrom(uom => uom.UnitSize));

【问题讨论】:

【参考方案1】:

到目前为止,您的尝试存在一些问题。

首先,您的 GetAllAsync 看起来像是一个 async 方法,但您让它完全同步调用,并且没有标记为 async。在您掌握了检索数据的基础知识之前,我会避免深入研究异步方法。

我们无法从您的示例中看到您的度量单位实体和视图模型之间的映射。实体在度量单位和 UnitSizes 之间具有一对多的关系,因此更新的内容取决于视图模型的布局和映射配置方式。这很可能是问题的根源,您的实体视图模型映射可能依赖于与您期望的数据不匹配的约定。

在性能方面,随着您的数据模型在实体和行方面的增长,这种方法会遇到问题。使用这样的存储库的根本问题是这样的方法:

IEnumerable<UnitOfMeasure> GetAllUnitsOfMeasure(string TenantId)

会将所有数据加载到内存中,并且您明确需要包含相关实体,无论消费者是否需要它们,这增加了查询需要完成的工作量和所需的内存。如果 TenantId 用于多租户数据库之类的东西,例如在具有多个租户使用单个数据源的 SaaS 应用程序中,这是采用存储库模式的一个很好的理由,但我不会将tenantIds 作为参数传递。相反,让存储库接受可以验证和解析会话中当前 TenantId 的依赖项。这样,存储库始终可以确保当前租户规则得到验证并应用于每个查询,而无需担心调用者可能从哪里获得 TenantId。 (即从 POST 请求中接受 TenantId 会很糟糕,因为该值很容易被篡改)

为了解决性能问题并可能触及您所读到的有关 IQueryable 扩展的内容,而不是从存储库返回 IEnumerable&lt;TEntity&gt;,您可以返回 IQueryable&lt;TEntity&gt;。这样做的好处是您仍然可以让存储库添加基本过滤规则,例如租户 ID,并允许使用者处理排序和投影等事情。

例如,存储库看起来更像:

public class UnitOfMeasureRepository : IUnitOfMeasureRepository

    private readonly MudmanDbContext _context;
    private readonly ICurrentUserLocator _currentUserLocator;

    public UnitOfMeasureRepository(MudmanDbContext context, ICurrentUserLocator currentUserLocator ) 
    
         _context = context ?? throw new ArgumentNullException("context");
         _currentUserLocator = currentUserLocator ?? throw new ArgumentNullException("currentUserLocator");
     

    public  IQueryable<UnitOfMeasure> GetUnitsOfMeasure()
    
         var tenantId = _currentUserLocator.CurrentUserTenantId; // Checks session for current user and retrieves a tenant ID or throws an exception. (no session, etc.)
         var query = _context.UnitOfMeasure
             .Where( uom => uom.TenantId == tenantId)

         return query;
     

这里要注意的更改是我们取消了基本的通用存储库类。这很令人困惑,因为您将上下文传递给基类,然后还设置了本地上下文实例。带有 EF 的通用存储库是一种不好的代码气味,因为它们会导致代码非常复杂、性能非常差,或两者兼而有之。容器可以注入的 CurrentUserLocator 是一个简单的类,可以验证用户当前是否经过身份验证并可以返回其租户 ID。从那里我们将返回一个IQueryable&lt;UnitOfMeasure&gt;,它有一个用于TenantID 的基本过滤器,这将允许我们的消费者决定他们想要如何使用它。请注意,我们不需要为相关实体使用Include,消费者可以再次决定他们需要什么。

调用新的存储库方法并投影您的视图模型看起来与您所拥有的非常相似。看起来您正在使用 Automapper,而不是使用 .Map(),我们可以将 .ProjectTo()IQueryable 一起使用,Automapper 基本上可以构建一个 Select() 表达式来仅拉回视图模型需要的数据。要使用ProjectTo 扩展方法,我们确实需要为它提供用于创建映射器的 MappingConfiguration,它会告诉它如何构建 ViewModel。 (因此,您需要为该映射器设置的 MapperConfiguration 而不是“映射器”类型的依赖项。)

public  IEnumerable<UnitOfMeasureViewModel> GetAll()
    
     var models =  _unitOfMeasureRepo.GetUnitsOfMeasure()
         .OrderBy(r => r.Name)
         .ProjectTo<UnitOfMeasureViewModel>(_mapperConfiguration)
         .ToList();

这样做是调用我们的存储库方法来获取IQueryable,然后我们可以附加我们想要的顺序,并调用ProjectTo 以允许Automapper 在使用ToList() 执行查询之前填充视图模型。当使用SelectProjectTo 时,我们无需担心使用Include 急切加载可能被映射的相关数据,这些方法会在需要时自动加载数据相关实体。

即使我们想使用这样的方法来更新具有相关实体的实体,使用IQueryable 也可以:

public void IncrementUnitSize(string unitOfMeasureId)
    
     var unitOfMeasure =  _unitOfMeasureRepo.GetUnitsOfMeasure()
         .Include(r => r.UnitSizes)
         .Where(r => r.Id == unitOfMeasureId)
         .Single();

     foreach(var unitSize in unitOfMeasure.UnitSizes)
         unitSize.UnitSize += 1;

     _context.SaveChanges();

仅作为根据需要获取相关实体的示例,而不是使用返回 IEnumerable 的方法并需要急切加载所有内容以防调用者可能需要它。

这些方法可以很容易地转换为异步方法,而无需触及存储库:

public async Task<IEnumerable<UnitOfMeasureViewModel>> GetAll()
    
     var models =  await _unitOfMeasureRepo.GetAllUnitsOfMeasure(TenantId)
         .OrderBy(r => r.Name)
         .ProjectTo<UnitOfMeasureViewModel>(_mapperConfiguration)
         .ToListAsync();

...仅此而已!请记住,async 不会使通话速度更快,如果有的话,它会使通话速度变慢。它所做的是通过允许服务器将请求处理移动到后台线程并释放请求线程来获取新的服务器请求,从而使服务器更具响应性。这对于需要一些时间或将被非常频繁地调用以避免捆绑所有服务器请求线程导致用户等待来自服务器响应的超时的方法非常有用。对于速度非常快且预计不会受到很多用户打击的方法,async 不会增加很多价值,您需要确保等待每个异步调用,否则您可能会遇到奇怪的行为和例外。

【讨论】:

以上是关于使用可查询扩展取回外键数据值的主要内容,如果未能解决你的问题,请参考以下文章

sql中外键怎么写?

可扩展数据库模式 - 如何存储可扩展属性值

SQL 数据库 子查询主外键

很菜的数据库问题,主键的值允许重复吗?外键啥作用?

django 插入外键值思路

EF6 - 使用可为空属性(外键,TPH)时的无效 SQL 查询