在实体框架中包含外键记录

时间:2020-07-23 20:43:59

标签: sql .net entity-framework

首先,请允许我道歉,我对EF非常陌生,并且继承了一些我几乎不了解的代码。因此,这可能不是措辞上最好的问题……但是它来了。

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

NoteTable:

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

NoteDetails

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; }

MetaDataOptions:

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

在查看代码的地方,NoteDetailsTable的数据已填写,而FrequencyId,WeightId和LengthID均已填写,并具有引用MetaDataOptions表的透视图ID。

然后代码执行一些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个Load事件(.Select(a => a.Frequency).Load(); ...)正在拉低整个记录的表。不仅是与注释关联的记录。当我将分析器放在SQL上时,我可以看到它运行以下语句,每次引用一次

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

我的问题是,我可以转换该load语句,以便它只为每个引用加载单个MetaDataOptions记录,并以与上述相同的方式将其附加到响应对象上。

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

1 个答案:

答案 0 :(得分:0)

是的,那些Load语句将触发将所有子孙读取到内存中。一旦跟踪了数据库上下文,当您加载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指向将实体结构转换为DTO结构的Automapper配置。

编辑:基于注释中的信息,假设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语句。