为什么一旦 SaveChanges ef 就失去了关系?

why ef lost relationship once SaveChanges?

如果我只是这样做:

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

它可以是什么?

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

更改跟踪

Entity Framework 在内存中有一种实体缓存,称为 更改跟踪器

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

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

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

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

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

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

延迟加载

您使用的代码表明您正在延迟加载。为了简单起见,我将在这里掩盖复杂的细节。如果你不知道什么是 lazy/eager 加载,我建议你查一下这个,因为解释太长了,不能写在这里。 Lazy/eager 加载是 Entity Framework 中处理实体关系以及如何获取相关实体的关键概念。

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

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

正在更新未跟踪的实体

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

 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 的 one 实体。由于 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 需要能够从您的实体 class 继承并创建一个特殊的派生 class 来覆盖导航 属性 行为。

当您说:

时,您已经偶然发现了这一点

I thing I've noticed: if I analyze medical object with QuickWatch the first time (without SaveChanges part) its as {System.Data.Entity.DynamicProxies.Medicals_650D310387E78A83885649345ED0FB2870EC304BF647B59321DFA0E4FBC78047}.

Instead, if I do SaveChanges and then I retrieve that medical, it is as {MyNamespace.Models.Medicals}.

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

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