【问题标题】:Do you have to use .Include to load child objects in EF Core 5?是否必须使用 .Include 加载 EF Core 5 中的子对象?
【发布时间】:2021-09-02 06:50:03
【问题描述】:

我习惯了 EF 6,我是 EF Core 的新手。我正在尝试从与查找表有一些外键关系的表中进行简单的提取。

我从这个开始:

List<StatementModel> myStatements = new List<StatementModel>();

myStatements = db.Statements.Select(s => new StatementModel
{
    StatmentId = s.StatementId,
    EmployeeNumber = s.EmployeeNumber,
    FirstName = s.Employee.FirstName,
    LastName = s.Employee.LastName,
    PlanType = s.Employee.PlanType != null ? s.Employee.PlanType.PlanTypeName : "",
    FiscalPeriod = s.FiscalPeriod.StartDate.ToString("yyyy-MM-dd") + " - " + s.FiscalPeriod.EndDate.ToString("yyyy-MM-dd"),
    CostCenterId = s.Employee.CostCenterId,
    RVPName = s.Employee.CostCenter.Evp != null ? s.Employee.CostCenter.Evp.FirstName + " " + s.Employee.CostCenter.Evp.LastName : "",
    SVPName = s.Employee.CostCenter.Svp != null ? s.Employee.CostCenter.Svp.FirstName + " " + s.Employee.CostCenter.Svp.LastName : "",
    LOBMgrName = s.Employee.CostCenter.Lobmgr != null ? s.Employee.CostCenter.Lobmgr.FirstName + " " + s.Employee.CostCenter.Lobmgr.LastName : "",
    AdminApprovalStatus = s.AdminApprovalStatus.ApprovalStatusName,
    StatementStatus = s.StatementStatus.StatementStatusName,
    AmountDue = s.AmountDue
}).Where(s => s.StatementStatusId == "PAA").ToList();

但它给了我一个运行时错误,说它无法将其转换为 SQL。我认为这是.ToString() 和日期格式的问题。

所以我改成:

List<StatementModel> myStatements = new List<StatementModel>();
var statements = db.Statements.Where(s => s.StatementStatusId == "PAA").ToList();

foreach (var s in statements)
{
    StatementModel sm = new StatementModel();

    sm.StatmentId = s.StatementId;
    sm.EmployeeNumber = s.EmployeeNumber;
    sm.FirstName = s.Employee.FirstName;
    sm.LastName = s.Employee.LastName;
    sm.PlanType = s.Employee.PlanType != null ? s.Employee.PlanType.PlanTypeName : "";
    sm.FiscalPeriod = s.FiscalPeriod.StartDate.ToString("yyyy-MM-dd") + " - " + s.FiscalPeriod.EndDate.ToString("yyyy-MM-dd");
    sm.CostCenterId = s.Employee.CostCenterId;
    sm.RVPName = s.Employee.CostCenter.Evp != null ? s.Employee.CostCenter.Evp.FirstName + " " + s.Employee.CostCenter.Evp.LastName : "";
    sm.SVPName = s.Employee.CostCenter.Svp != null ? s.Employee.CostCenter.Svp.FirstName + " " + s.Employee.CostCenter.Svp.LastName : "";
    sm.LOBMgrName = s.Employee.CostCenter.Lobmgr != null ? s.Employee.CostCenter.Lobmgr.FirstName + " " + s.Employee.CostCenter.Lobmgr.LastName : "";
    sm.AdminApprovalStatus = s.AdminApprovalStatus.ApprovalStatusName;
    sm.StatementStatus = s.StatementStatus.StatementStatusName;
    sm.AmountDue = s.AmountDue;

    myStatements.Add(sm);
}

然后我得到了一堆空引用错误,例如Employee.CostCenterFiscalPeriod.StartDate 之类的任何子对象。

然后我把它改成:

var statements = db.Statements.Include("Employee.CostCenter.Evp")
              .Include("Employee.CostCenter.Svp")
              .Include("Employee.CostCenter.Lobmgr")
              .Include("FiscalPeriod")
              .Include("AdminApprovalStatus")
              .Include("StatementStatus").Where(s => s.StatementStatusId == "PAA").ToList();

foreach (var s in statements)
{
    StatementModel sm = new StatementModel();

    sm.StatmentId = s.StatementId;
    sm.EmployeeNumber = s.EmployeeNumber;
    sm.FirstName = s.Employee.FirstName;
    sm.LastName = s.Employee.LastName;
    sm.PlanType = s.Employee.PlanType != null ? s.Employee.PlanType.PlanTypeName : "";
    sm.FiscalPeriod = s.FiscalPeriod.StartDate.ToString("yyyy-MM-dd") + " - " + s.FiscalPeriod.EndDate.ToString("yyyy-MM-dd");
    sm.CostCenterId = s.Employee.CostCenterId;
    sm.RVPName = s.Employee.CostCenter.Evp != null ? s.Employee.CostCenter.Evp.FirstName + " " + s.Employee.CostCenter.Evp.LastName : "";
    sm.SVPName = s.Employee.CostCenter.Svp != null ? s.Employee.CostCenter.Svp.FirstName + " " + s.Employee.CostCenter.Svp.LastName : "";
    sm.LOBMgrName = s.Employee.CostCenter.Lobmgr != null ? s.Employee.CostCenter.Lobmgr.FirstName + " " + s.Employee.CostCenter.Lobmgr.LastName : "";
    sm.AdminApprovalStatus = s.AdminApprovalStatus.ApprovalStatusName;
    sm.StatementStatus = s.StatementStatus.StatementStatusName;
    sm.AmountDue = s.AmountDue;

    myStatements.Add(sm);
}

这可行,但它非常冗长,我不记得曾经在 EF 6 中显式地 .Include 事情,除非这是一个优化问题。

这是在 EF Core 中唯一或最好的方法还是我遗漏了什么?

【问题讨论】:

  • This works, but it's very verbose and I don't recall ever having to explicitly .Include things in EF 6 unless it was an optimization issue 这是因为默认情况下在 EF6 中的 DbContext 实例上启用了延迟加载。如果你禁用了这个,那么你还必须使用 .Include。
  • @ChrisSchaller - 如果您仍想为通过投影收到的第一个异常发布解决方案,请告诉我,我将重新提出问题。
  • @Legion - 另请参阅canonical functions,它可以帮助您确定在编写需要通过 linq to entity 转换为 sql 的代码时支持和不支持的内容。
  • @ChrisSchaller 我重新打开了这个问题。
  • @ChrisSchaller 有很多方法可以给猫剥皮。你的回答也完全可以接受。对于 OP:投影可以导致比使用包含更好的查询,但我总是会使用 Sql Profiler 进行检查,并注意读取次数和持续时间以及查询本身。在您的机器上可能没问题,但由于数据量等多种原因导致生产速度变慢。不过,这只是一个旁注。

标签: c# entity-framework-core


【解决方案1】:

这里有几个单独的问题,但总体问题是您被困在尝试选择数据库 (SQL) 来管理 DTO 投影或本地 C# 运行时。

  • 由于 DateTime.ToString() 语法不正确,您的延迟 SQL 版本(初始尝试)失败。
  • C# 版本在.Include() 方面是冗长的,混合了魔术字符串,但效率也非常低,您可能会拉回潜在的数百列,当您将结果投影到时却忽略了大部分列DTO

如需特别针对 .Include 的帮助,请查看这些之前的帖子:

对于此类问题,您应该考虑一种混合方法,一种尊重并利用两种环境优势的方法。

  1. 投影您的逻辑所需的 DB 元素,但将它们保持为原始格式,让 EF 和 SQL 协同工作以优化输出。

    注意:不需要形式化这个投影模型,我们可以简单地使用匿名类型!

     List<StatementModel> myStatements = new List<StatementModel>();
    
     var dbQuery = db.Statements.Select(s => new {
         s.StatementId,
         s.EmployeeNumber,
         s.Employee.FirstName,
         s.Employee.LastName,
         s.Employee.PlanType.PlanTypeName,
         s.FiscalPeriod.StartDate,
         s.FiscalPeriod.EndDate,
         s.Employee.CostCenterId,
         Evp = new {
             s.Employee.CostCenter.Evp.FirstName,
             s.Employee.CostCenter.Evp.LastName
         },
         Svp = new { 
             s.Employee.CostCenter.Svp.FirstName,
             s.Employee.CostCenter.Svp.LastName
         },
         LOBMgr = new { 
             s.Employee.CostCenter.Lobmgr.FirstName,
             s.Employee.CostCenter.Lobmgr.LastName
         },
         s.AdminApprovalStatus.ApprovalStatusName,
         s.StatementStatus.StatementStatusName,
         s.AmountDue
     }).Where(s => s.StatementStatusId == "PAA");
    

    没有必要嵌套Evp,Svp,LOBMgr 投影,您可以展平整个结果集,实际上效率会稍微高一点,但这显示了可能性 em>

  2. 现在将结果集投影到内存中的 DTO 中,这样您就可以完全控制 C# 类型转换和字符串格式化。

     myStatements = dbQuery.ToList()
                           .Select(s => new StatementModel
     {
         StatmentId = s.StatementId,
         EmployeeNumber = s.EmployeeNumber,
         FirstName = s.FirstName,
         LastName = s.LastName,
         PlanType = s.PlanTypeName ?? "",
         FiscalPeriod = $"{s.StartDate:yyyy-MM-dd} - {s.EndDate:yyyy-MM-dd}",
         CostCenterId = s.CostCenterId,
         RVPName = $"{s.Evp.FirstName} {s.Evp.LastName}".Trim(),
         SVPName = $"{s.Svp.FirstName} {s.Svp.LastName}".Trim(),
         LOBMgrName = $"{s.LOBMgr.FirstName} {s.LOBMgr.LastName}".Trim(),
         AdminApprovalStatus = s.ApprovalStatusName,
         StatementStatus = s.StatementStatusName,
         AmountDue = s.AmountDue
     }).ToList();
    

请注意,没有包含!我们将 Includes 替换为初始 Projection。 IMO 我发现这段代码更自然,包含可能非常间接,为什么我们需要它们或者我们根本忘记添加它们并不总是很明显。这段代码感觉有点像双重处理,但我们只带回我们需要的特定列,而不必通过尝试让数据库格式化数据值来妨碍干净的 SQL,因为我们可以用最少的时间做到这一点在 C# 中的努力。

日期格式化很简单,但如果您需要对 c# 中已有的结果执行复杂的格式化或其他处理逻辑,而不将该逻辑复制到 SQL 友好的 Linq 中,则此技术可能非常强大。


避免包含中的魔术字符串

如果您打算使用包含,您应该尽量避免包含的字符串变体,而是使用 lambda。这将允许编译器在架构更改可能使您的查询无效时通知您或以后的开发人员:

    .Include(x => x.Employee.CostCenter.Evp)
    .Include(x => x.Employee.CostCenter.Svp)
    .Include(x => x.Employee.CostCenter.Lobmgr)
    .Include(x => x.FiscalPeriod)
    .Include(x => x.AdminApprovalStatus)
    .Include(x => x.StatementStatus) 

【讨论】:

  • 很好的答案!谢谢!
猜你喜欢
  • 2021-03-09
  • 2019-08-05
  • 1970-01-01
  • 2021-04-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-19
相关资源
最近更新 更多