【问题标题】:why ef lost relationship once SaveChanges?为什么 ef 一旦 SaveChanges 就失去了关系?
【发布时间】:2021-03-31 12:11:36
【问题描述】:

如果我只是这样做:

var medical = ctx.Medicals.FirstOrDefault(p => p.ID == medicalViewModel.ID);
var sizeClinics = medical.Clinics.Count;

金额为(例如)10 个(即我有 10 个诊所用于该医疗)。 现在,如果我这样做:

var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
ctx.Entry(medical).State = medical.ID == 0 ? EntityState.Added : EntityState.Modified;
ctx.SaveChanges();

medical = ctx.Medicals.FirstOrDefault(p => p.ID == medicalViewModel.ID);
var sizeClinics = medical.Clinics.Count;

大小为0。为什么? SaveChanges 后似乎删除了关系?

这是 Medicals 对象:

public partial class Medicals
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
    public Medicals()
    {
        this.Activities = new HashSet<Activities>();
        this.MedicalsRefunds = new HashSet<MedicalsRefunds>();
        this.Clinics = new HashSet<Clinics>();
    }

    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<Activities> Activities { get; set; }
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<MedicalsRefunds> MedicalsRefunds { get; set; }
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<Clinics> Clinics { get; set; }
}

我注意到了:如果我第一次使用 QuickWatch 分析医疗对象(没有 SaveChanges 部分),它是 {System.Data.Entity.DynamicProxies.Medicals_650D310387E78A83885649345ED0FB2870EC304BF647B59321DFA0E4FBC78047}。

相反,如果我执行 SaveChanges 然后检索该医疗,它是 {MyNamespace.Models.Medicals}。

会是什么?

【问题讨论】:

  • 请向我们展示您的医疗实体代码。
  • 您的查询尝试返回单个对象,而不是关系。您的代码要么插入一个新实体,在这种情况下ID 可能会更改,要么您更新一个已经存在的实体。我怀疑这是第一种情况 - ID 是数据库生成的,可能是 IDENTITY,medicalViewModel.ID0
  • @PWND 添加了 Medicals 类
  • @Panagiotis Kanavos 我正在(在示例中)使用现有项目,因此在这两种情况下, medicalViewModel.ID (例如)都是 14。
  • 分离的对象是否有任何诊所?

标签: c# .net entity-framework relationship


【解决方案1】:

通过了解 Entity Framework 如何在内部工作来回答这个问题。我将尝试在此处突出显示主要功能。

更改跟踪

Entity Framework 在内存中有一种实体缓存,称为change tracker

在您的第一个示例中,当您从数据库中获取实体时:

var medical = ctx.Medicals.FirstOrDefault(p => p.ID == medicalViewModel.ID);

Entity Framework 创建您收到的Medicals 实例。当它这样做时,它也会利用这个机会来存储对该对象的引用,出于自己的原因。它将密切关注这些对象并跟踪对它们所做的任何更改。

例如,如果您现在随时调用 ctx.SaveChanges();,它会查看其更改跟踪器中的所有内容,查看哪些内容已更改,并更新数据库中的内容。

这样做有几个好处:您不必明确告诉 EF 您对其缓存中已经跟踪的某些实体进行了更改,并且 EF 还可以发现哪些特定字段已更改,因此它只需要更新那些特定的字段,它可以忽略未更改的字段。

从 cmets 更新:EF 仅允许基于 PK 值跟踪给定实体的 一个 实例。因此,如果您已经跟踪了 ID 为 123 的 Medical,则无法跟踪 ID 为 123 的同一 Medical 实体的另一个实例。

延迟加载

您使用的代码表明您是延迟加载。我将在这里掩盖复杂的细节,以保持简单。如果您不知道什么是延迟/急切加载,我建议您查看此内容,因为解释太长,无法在此处写下。 Lazy/eager loading 是 Entity Framework 中处理实体关系以及如何获取相关实体的关键概念。

在处理延迟加载时,EF 在为您获取实体时会稍微修改它。它在所有实体的导航属性中放置了一个特殊的惰性集合(例如medical.Clinics),因此它只会在您实际尝试访问它时获取相关数据,即通过枚举集合任何方式。

相比之下,如果您使用预加载,EF 不会为您执行此操作,并且 nav 道具根本不会填充任何内容,除非您明确调用 Include

更新未跟踪的实体

在您的第二个示例中,您正在使用不是由实体框架创建的实体对象。你自己做的:

 var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);

现在您手动将其添加到更改跟踪器中:

ctx.Entry(medical).State = medical.ID == 0 ? EntityState.Added : EntityState.Modified;

这并没有错,但是您必须意识到更改跟踪器中的实体不是由 EF 生成的,因此它不包含这些特殊的“惰性导航属性”。而且因为它不包含这些惰性导航属性...

var sizeClinics = medical.Clinics.Count;

... 上面的代码实际上并没有尝试从数据库中获取数据。它只适用于您生成的实体对象以及它已包含在内存中的内容。

由于您自己没有向medical.Clinics 添加任何内容,因此该集合为空。

答案

延迟加载仅适用于 EF 生成的实体对象,而不适用于您生成的实体对象,无论您之后是否手动将其添加到 EF 的更改跟踪器。

所以要获取计数,可以专门从数据库中查询诊所:

var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
var clinicCount = ctx.Clinics.Count(p => p.MedicalId == medical.ID);

或者你可以分离实体并从数据库中获取它,虽然我不喜欢这个:

var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
ctx.Entry(medical).State = medical.ID == 0 ? EntityState.Added : EntityState.Modified;
ctx.SaveChanges();

// Detach
ctx.Entry(medical).State = EntityState.Detached;

// Now fetch from db
var medical2 = ctx.Medicals.FirstOrDefault(p => p.ID == medical.ID);
var sizeClinics = medical2.Clinics.Count;

为什么要分离?请记住我曾提到 EF 只允许跟踪给定类型和 PK 的 一个 实体。由于medical 引用的对象已被跟踪,因此您无法获取和跟踪具有相同PK 的Medicals 的另一个新实例。
通过分离第一个实例,medical2 可以被获取和跟踪,因为更改跟踪器“忘记了”另一个实例。

但老实说,只打开一个新上下文而不是尝试手动分离和重新查询会更容易。

var medical = mapper.Map<MedicalViewModel, Medicals>(medicalViewModel);
ctx.Entry(medical).State = medical.ID == 0 ? EntityState.Added : EntityState.Modified;
ctx.SaveChanges();

using(var ctx2 = new MyContext())
{
    var medical2 = ctx2.Medicals.FirstOrDefault(p => p.ID == medical.ID);
    var sizeClinics = medical2.Clinics.Count;
}

如果您有兴趣了解更多信息

如果您首先使用代码,延迟加载就是 EF 要求您创建这些属性 virtual 的原因。 EF 需要能够从您的实体类继承并创建一个覆盖导航属性行为的特殊派生类。

你已经偶然发现了这个,当你说:

我注意到了:如果我第一次使用 QuickWatch 分析医疗对象(没有 SaveChanges 部分),它是 {System.Data.Entity.DynamicProxies.Medicals_650D310387E78A83885649345ED0FB2870EC304BF647B59321DFA0E4FBC78047}。

相反,如果我执行 SaveChanges 然后检索该医疗,它是 {MyNamespace.Models.Medicals}。

System.Data.Entity.DynamicProxies.Medicals_65(等等)类是由 Entity Framework 动态生成的,继承了Medicals 类,并覆盖了virtual 导航属性,以便在枚举集合时延迟加载此信息。

这就是 EF 如何实现延迟加载的隐藏魔法。

【讨论】:

  • 但是为什么在第二种解决方案中,我需要分离医疗?稍后我将使用 var medical2,它是一个新的“对象”。所以将从头开始并从数据库中获取数据...
  • @markzzz:EF 将仅跟踪 一个 实体(基于其 PK)。因此,如果您已经跟踪了 ID 为 123 的 Medical,则无法跟踪 ID 为 123 的同一 Medical 实体的另一个实例。我已使用此信息更新了答案。
  • 现在你已经开始了,也许有趣的是,ctx2.Medicals.Create() 创建了一个 EF 代理对象。
  • @GertArnold:我什至不知道! :) 那么像var m = ctx.Medicals.Create(); m.Id = 123; var clinicsCount = m.Clinics.Count; 这样的东西会起作用吗?
  • 如果 m 附加到上下文中,是的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-19
  • 2016-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-17
相关资源
最近更新 更多