实体框架6 -多个查找插入vs .具有相同键的对象已经存在于ObjectStateManager中

本文关键字:对象 ObjectStateManager 存在 框架 查找 vs 插入 实体 | 更新日期: 2023-09-27 17:54:32

我显然有一个真正的魔鬼时间理解实体框架6,我正在使用ASP。. NET MVC 5.

问题的核心是,我有一个非常简单的数据模型,这是典型的任何现实世界的情况下,我有各种业务对象有其他业务对象作为属性(当然,他们的子对象可能反过来有其他子业务对象),也有各种类型的查找/类型数据(国家,州/省,语言类型,StatusType等),我不知道如何保存/更新它正确。

我一直在两个错误状态之间来回切换

1)我要么遇到这样的情况:保存父业务对象导致不需要的重复值被插入到我的查找/类型表中(例如,保存已被分配为'English'的现有LanguageType的业务对象将导致另一个用于'English'的LanguageType被插入到LanguageType表中),要么

2)我使用一些我在这里看到的建议和其他地方在网上(例如,保存实体导致重复插入到查找数据,防止实体框架插入值导航属性)来解决问题1,然后发现自己对这个问题的斗争:具有相同键的对象已经存在于ObjectStateManager。ObjectStateManager不能跟踪具有相同key的多个对象。

现在我将提供一些代码片段来帮助构建我正在尝试做的事情以及我正在使用的事情的图片。首先,一个涉及实体的例子:

<>之前 public class Customer : BaseEntity { public string Name { get; set; } [LocalizedDisplayName("Contacts")] public virtual List Contacts { get; set; } } public class Contact : BaseEntity { [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public int? LanguageTypeID { get; set; } [Required] [ForeignKey("LanguageTypeID")] public virtual LanguageType Language { get; set; } } public class LanguageType : Lookup { [LocalizedDisplayName("CultureName")] public string CultureName { get; set; } } public class Lookup : BaseEntity { public string DisplayName { get; set; } public int DisplayOrder { get; set; } public string Name { get; set; } public string Description { get; set; } } public class BaseEntity { public int ID { get; set; } public DateTime? CreatedOn { get; set; } public DateTime? UpdatedOn { get; set; } public DateTime? DeletedOn { get; set; } public bool Deleted { get; set; } public bool Active { get; set; } public ApplicationUser CreatedByUser { get; set; } public ApplicationUser UpdatedByUser { get; set; } } 之前

在我的控制器中,我有如下代码:

<>之前 foreach(Contact contact in lstContacts) { customer.Contacts.Add(contact); } if (ModelState.IsValid) { repository.Add(customer); } 之前

让我们假设每个联系人都分配了相同的LanguageType 'English'(在这个例子中,我试图保存多个具有相同LanguageType触发ObjectStateManager错误的联系人)。最初,repository.Add()代码只是执行了一个没有按预期工作的context.SaveChanges(),所以现在它看起来像这样(实体变量是一个Customer):

<>之前 try { if(Entity.Contacts != null) { foreach(Contact contact in Entity.Contacts) { var entry = this.context.Entry(contact.Language); var key = contact.Language.ID; if (entry.State == EntityState.Detached) { var currentEntry = this.context.LanguageTypes.Local.SingleOrDefault(l => l.ID == key); if (currentEntry != null) { var attachedEntry = this.context.Entry(currentEntry); //attachedEntry.CurrentValues.SetValues(entityToUpdate); attachedEntry.State = EntityState.Unchanged; } else { this.context.LanguageTypes.Attach(contact.Language); entry.State = EntityState.Unchanged; } } } } context.Customers.Add(Entity); context.SaveChanges(); } catch(Exception ex) { throw; } 之前

期望它已经工作,这是根本错误的吗?我该如何保存这样一个例子呢?我有类似的问题保存类似的对象图。当我看EF的教程和例子时,它们都很简单,它们都只是在做了一些与我在这里做的非常相似的事情后调用SaveChanges()。

我最近一直在使用ColdFusion的ORM功能(它在后台是休眠的),它可以简单地加载LanguageType实体,将其分配给Contact实体,保存Contact实体,将其分配给Customer,然后保存Customer。

在我看来,这是最基本的情况,我无法相信它给我带来了这么多的痛苦——我讨厌这么说,但使用普通的旧ADO。NET(或者我真的不喜欢的ColdFusion)会简单得多。所以我错过了一些东西。显然,我对英孚的理解/方法有一个关键的缺陷,如果有人能帮助我使这件事按预期进行,并帮助我找出我的误解所在,我将不胜感激。我在这上面花了太多的时间,这是浪费时间——在我正在构建的代码中,我已经/将会有无数这样的例子,所以我现在需要调整我对EF的看法,这样我就可以高效地工作,并以预期的方式处理事情。

你的帮助太重要了,我非常感谢你!

实体框架6 -多个查找插入vs .具有相同键的对象已经存在于ObjectStateManager中

让我们考虑以下对象图,其中教师实例是对象,

老师——[有许多]——>课程

Teacher -[Has One]->部门

在实体框架的DbContext中,对象的每个实例都有一个State,表示该对象是AddedModifiedRemoved还是Unchanged。显然发生的事情如下:

第一次创建根对象

在这种情况下,除了新创建的根对象Teacher之外,图中的所有子对象也将具有State Added,即使它们已经创建。此问题的解决方案是为每个子元素包含外键属性并使用它,例如Teacher.DepartmentId = 3

更新根对象和它的一个子元素的属性

假设你从数据库中获取了一个teacher对象,并且你改变了Teacher.NameTeacher.Department.Name属性;在这种情况下,只有教师根对象将把State标记为Modified,另一方面,部门的State仍然是Unchanged,修改不会持久化到DB中;静默,没有任何警告。


编辑1

我使用你的类如下,我没有一个问题与持久化对象:

public class Customer : BaseEntity
{
    public string Name { get; set; }
    public virtual List<Contact> Contacts { get; set; }
}
public class Contact : BaseEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? LanguageTypeID { get; set; }
    public Customer Customer { get; set; }
    [ForeignKey("LanguageTypeID")]
    public LanguageType Language { get; set; }
}
public class LanguageType : Lookup
{
    public string CultureName { get; set; }
}
public class Lookup : BaseEntity
{
    public string DisplayName { get; set; }
    public int DisplayOrder { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}
public class BaseEntity
{
    public int ID { get; set; }
    public DateTime? CreatedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public DateTime? DeletedOn { get; set; }
    public bool Deleted { get; set; }
    public bool Active { get; set; }
    public ApplicationUser CreatedByUser { get; set; }
    public ApplicationUser UpdatedByUser { get; set; }
}
public class ApplicationUser
{
    public int ID { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
}

并使用以下上下文:

public class Context : DbContext
{
    public Context() : base("name=CS") { }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Contact> Contacts { get; set; }
    public DbSet<LanguageType> LanguageTypes { get; set; }
    public DbSet<ApplicationUser> ApplicationUsers { get; set; }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //I'm generating the database using those entities you defined;
        //Here we're demanding not add 's' to the end of table names
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }
}

然后创建了一个单元测试类,如下所示:

[TestMethod]
public void TestMethod1()
{
    //our context
    var ctx = new Infrastructure.EF.Context();
    
    //our language types
    var languageType1 = new LanguageType { ID = 1, Name = "French" };
    var languageType2 = new LanguageType { ID = 2, Name = "English" };
    ctx.LanguageTypes.AddRange(new LanguageType[] { languageType1, languageType2 });
    
    //persist our language types into db before we continue.
    ctx.SaveChanges();
    //now we're about to start a new unit of work
    var customer = new Customer
    {
        ID = 1,
        Name = "C1",
        Contacts = new List<Contact>() //To avoid null exception
    };
    //notice that we're assigning the id of the language type and not
    //an object.
    var Contacts = new List<Contact>(new Contact[] { 
         new Contact{ID=1, Customer = customer, LanguageTypeID=1},
         new Contact{ID=2, Customer = customer, LanguageTypeID=2}
        });
    customer.Contacts.AddRange(Contacts);
    
    //adding the customer here will mark the whole object graph as 'Added'
    ctx.Customers.Add(customer);
    //The customer & contacts are persisted, and in the DB, the language
    //types are not redundant.
    ctx.SaveChanges();
}

一切都很顺利,没有任何问题。

据我所知,没有内置支持重新连接修改过的图形(如nHibernate的SaveOrUpdate方法)。也许这个或这个能帮到你。