【问题标题】:Include Foreign key records in Entity Framework在实体框架中包含外键记录
【发布时间】:2020-07-23 20:43:59
【问题描述】:

首先,请允许我道歉,我对 EF 还很陌生,并且继承了一堆我几乎看不懂的代码。所以这可能不是最好的措辞问题......但它就是这样。

我将 Dotnet Core 与 EF 结合使用,并为每个类进行了以下设置。

笔记表:

public int NoteId { get; set;}
public string Description { get; set; } 
public List<NoteDetails> NoteDetails {get; set; }

注意事项

public string Description { get; set; }
public int NoteDetailId { get; set; }
public int NoteId { get; set; }
public MetaDataOptions Frequency { get; set; }
public int? FrequencyId { get; set; }
public MetaDataOptions Width { get; set; }
public int? WidthId { get; set; }
public MetaDataOptions Length { get; set; }
public int? LengthId { get; set; }

元数据选项:

public int MetaDataId { get; set; }
public string Description { get; set; }
public int? DisplayOrder { get; set; }
public bool IsActive { get; set; }

在我看代码的地方,NoteDetailsTable 的数据已填写完毕,FrequencyId、WeightId 和 LengthID 均填写了它们的透视 ID,参考 MetaDataOptions 表。

然后代码会执行一些 EF Magic:

_clinicalSummaryDbContext.NoteDetails.Select(a => a.Frequency).Load();
_clinicalSummaryDbContext.NoteDetails.Select(a => a.Width).Load();
_clinicalSummaryDbContext.NoteDetails.Select(a => a.Height).Load();

响应对象看起来像:

{
   "NoteID":1,
   "Description":"First Note",
   "NoteDetails":[
      {
         "NoteId":1,
         "FrequencyId":1,
         "Frequency":{
            "RefTableId":1,
            "description":"Frequency Desciption"
         },
         "WidthId":2,
         "Width":{
            "RefTableId":2,
            "description":"Width Description"
         },
         "HeightID":3,
         "Height":{
            "RefTableId":3,
            "description":"Height Description"
         }
      }
   ]
 }

这正是我想要的。然而,问题是我们的性能团队注意到 3 个加载事件 (.Select(a => a.Frequency).Load();...) 正在拉下整个 noteetails 表。不仅仅是与笔记相关的记录。当我在 SQL 上放置一个分析器时,我可以看到它运行以下语句,每个引用一次

SELECT [w].[WoundDetailId] ...
FROM [NoteDetail] AS [w]
LEFT JOIN [MetaDataOptions] AS [w.Frequency] ON [w].[FrequencyId] = [w.Frequency].[MetaDataOptionId]

我的问题是,我能否转换该加载语句,使其仅加载每个引用的单个 MetaDataOptions 记录,并以与上述相同的方式将其附加到响应对象。

我希望我解释得足够好。很高兴提供任何有帮助的其他信息。

【问题讨论】:

    标签: sql .net entity-framework


    【解决方案1】:

    是的,那些Load 语句会触发将所有孙子读取到内存中。一旦 DB Context 对它们进行了跟踪,当您加载 Note/NoteDetail 时,它将神奇地填充这些相关实体。从性能和资源利用率 POV 来看,这是非常浪费的,并且在使用有限数据集的开发过程中很容易被忽视,随着数据集的增长,随着时间的推移在生产中会变得更糟。

    听起来他们想要的是用 Note 急切地加载详细信息。例如,如果我有:

    var note = _clinicalSummaryDbContext.Notes
        .Include(n => n.NoteDetails)
        .Single(n => n.NoteId == noteId);
    

    这将加载单个音符及其详细信息,但不会加载每个详细信息的频率/宽度/高度。

    要获得 EF Core 中包含的内容:

    var note = _clinicalSummaryDbContext.Notes
        .Include(n => n.NoteDetails).ThenInclude(nd => nd.Frequency)
        .Include(n => n.NoteDetails).ThenInclude(nd => nd.Width)
        .Include(n => n.NoteDetails).ThenInclude(nd => nd.Height)
        .Single(n => n.NoteId == noteId);
    

    重新包含 NoteDetails 看起来有点时髦,但对于多个孙子引用来说是必要的,坦率地说,这不是那么直观,但 EF 会仔细考虑加载细节,包括它们各自的频率、宽度和高度一个查询。

    或者,当加载将发送到视图或 API 客户端的数据时,通常最好发送实体,而是发送从相关实体投影的 DTO/ViewModel。发送实体通常涉及向客户端发送比客户端需要更多的数据,并且会透露太多关于您的整个域的信息。 (将实体发回服务器会带来严重的数据完整性风险)

    例如,您可以有一个带有 NoteDetailDTO 的 NoteDTO,其中相关的频率/宽度/高度字段被展平到 NoteDetailDTO 中,或者由 DTO 本身表示。可以使用 .Select 语句填充 DTO,而无需任何 .Include.ThenInclude,或者您可以使用 Automapper 及其 .ProjectTo 将 EF 实体转换为 DTO。 ProjectTo 与 EF IQueryable 一起生成 SQL 以填充相关字段。

    当映射设置为将 Note 实体结构转换为 NoteDTO 实体结构时,您最终会得到如下内容:

    var noteDTO = _clinicalSummaryDbContext.Notes
        .Where(n => n.NoteId == noteId)
        .ProjectTo<NoteDTO>(configuration)
        .Single();
    

    configuration 指向 Automapper 配置,用于将实体结构转换为 DTO 结构。

    编辑:根据评论中的信息,假设 Note 和 NoteDetail 已经加载,另一种选择是将 .Load() 调用替换为以下内容:

    // Find missing references for Frequencies, widths, and height metadata.
    var frequencyIds = note.NoteDetails
        .Where(x => x.FrequencyId.HasValue && x.Frequency == null)
        .Select(x => x.FrequencyId)
        .ToList();
    var widthIds = note.NoteDetails
        .Where(x => x.WidthId.HasValue && x.Width == null)
        .Select(x => x.WidthId)
        .ToList();
    var heightIds = note.NoteDetails
        .Where(x => x.HeightId.HasValue && x.Height == null)
        .Select(x => x.HeightId)
        .ToList();
    var missingMetadataOptionIds = frequencyIds.Concat(widthIds).Concat(heightIds);
    
    var missingMetadata = _clinicalSummaryDbContext.MetadataOptions
        .Where(x => missingMetadataOptionIds.Contains(x.MetadataOptionId))
        .ToList();
    
    foreach(var noteDetail in note.NoteDetails)
    {
       if(noteDetail.Frequency == null)
           noteDetail.Frequency = missingMetadata.SingleOrDefault(x => x.MetadataOptionId == noteDetail.FrequencyId);
       if(noteDetail.Width == null)
           noteDetail.Width = missingMetadata.SingleOrDefault(x => x.MetadataOptionId == noteDetail.WidthId);
       if(noteDetail.Height == null)
           noteDetail.Height = missingMetadata.SingleOrDefault(x => x.MetadataOptionId == noteDetail.HeightId);
    }
    

    这假设这些 Load 调用是在加载 Note/NoteDetails 之后发生的。如果加载调用在 Note 加载之前发生,那么可能的最佳可用解决方案是添加即时加载 Include/ThenInclude 语句。

    【讨论】:

    • 总的来说,这非常有帮助,谢谢!不幸的是,我不确定这对我目前的情况有帮助。我被要求修复的代码在到达我要修复的函数时已经加载了修改后的 Notes 和 noteDetails 版本。因此,我将无法进行新的 EF 调用来获取带有 noteetails 和包含的元数据选项的笔记。
    • 我已经扩展了答案以涵盖替换 Load 调用的可能解决方案,如果这些调用是在加载 Note/NoteDetails 之后发生的。基本上获取缺失实体的 ID 并仅加载那些元数据,然后通过并设置缺失的引用。这也假设 Note/NoteDetails 是 not 跟踪的实体,我猜它们不是,或者它们会延迟加载。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-01-27
    • 1970-01-01
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多