Entity Framework 6 - Множественные вставки поиска vs Объект с тем же ключом уже существует в ObjectStateManager

asp.net-mvc asp.net-mvc-5 c# entity-framework entity-framework-6

Вопрос

У меня, видимо, настоящий дьявол понимания времени Entity Framework 6, который я использую с ASP.NET MVC 5.

Суть в том, что у меня действительно очень простая модель данных, характерная для любой ситуации в реальном мире, где у меня есть различные бизнес-объекты, которые имеют другие бизнес-объекты в качестве свойств (и, конечно же, у них, в свою очередь, у других дочерних предприятий объекты), а также различные типы данных поиска / типа (страна, штат / провинция, тип LanguageType, StatusType и т. д.), и я не могу понять, как сохранить / обновить его правильно.

Я продолжаю идти туда и обратно между двумя состояниями ошибки:

1) Я либо сталкиваюсь с ситуацией, когда сохранение родительского бизнес-объекта приводит к тому, что ненужные повторяющиеся значения вставляются в мои таблицы lookup / type (например, сохранение бизнес-объекта, которому был присвоен существующий LanguageType «английский», приведет к другому языковому типу для «английского» вставляется в таблицу LanguageType) или

2) Я использую некоторые из предложений, которые я видел здесь и в других местах в сети (например, Saving Entity вызывает дублируемую вставку в данные поиска , Запрет Entity Framework вставлять значения для навигационных свойств ) для решения проблемы 1, а затем обнаруживать, что я борюсь с этим такая же проблема: объект с тем же ключом уже существует в ObjectStateManager. ObjectStateManager не может отслеживать несколько объектов с одним и тем же ключом .

Теперь я предоставлю несколько фрагментов кода, чтобы помочь составить картину того, что я пытаюсь сделать, и что я использую для этого. Во-первых, пример вовлеченных объектов:



    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 «английский», назначенный (и в этом примере я пытаюсь сохранить несколько контактов с одним и тем же языком, который вызывает ошибку ObjectStateManager). Первоначально код repository.Add () просто сделал context.SaveChanges (), который не работал должным образом, поэтому теперь он выглядит примерно так (переменная Entity - это Клиент):



    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 () после того, как делают что-то очень похожее на то, что я здесь делаю.

Недавно я использовал возможности ORM ColdFusion (который находится в режиме спящего режима под обложками), и они просто загружают объект LanguageType, назначают его объекту Contact, сохраняют объект Contact, назначают его клиенту, а затем сохраняют клиент.

На мой взгляд, это самые основные ситуации, и я не могу поверить, что это вызвало у меня столько боли - я ненавижу говорить об этом, но используя простой старый ADO.NET (или не дай бог, ColdFusion, который мне действительно не нравится ) было бы намного проще. Так что я скучаю ЧТО-ТО. У меня, по-видимому, есть ключевой недостаток в моем понимании / подходе к EF, и если кто-то может помочь мне сделать эту работу ожидаемой и помочь мне разобраться, где мое недоразумение, я бы очень признателен. Я трачу слишком много часов и часов на это, и это пустая трата времени. У меня есть / будет иметь бесчисленные примеры, подобные этому в коде, который я создаю, поэтому мне нужно сейчас настроить мои мысли относительно EF, поэтому я могут быть продуктивными и приближаться к ним ожидаемым образом.

Ваша помощь будет означать так много, и я благодарю вас за это!

Принятый ответ

Рассмотрим следующий граф объектов, в котором экземпляр учителя является корневым объектом,

Учитель - [имеет много] -> курсы

Учитель - [Имеет один] -> Отдел

В DbContext сущности DbContext каждый экземпляр объекта имеет State указывающее, будет ли объект Added , Modified , Removed или не Unchanged . Очевидно, что происходит следующее:

Создание корневого объекта в первый раз

В этом случае в дополнении к недавно созданному корневому объекту Teacher , все дочерние объекты в графе будут иметь State Added и даже если они уже созданы. Решение этой проблемы состоит в том, чтобы включить свойство внешнего ключа для каждого дочернего элемента и использовать его вместо этого, например, Teacher.DepartmentId = 3 .

Обновление корневого объекта и свойств его дочерних элементов

Предположим, вы извлекли объект учителя из db, и вы изменили свойство Teacher.Name а также свойство Teacher.Department.Name ; в этом случае, только корень учителя объект будет иметь State помечено как Modified , Департамент State , с другой стороны , остается Unchanged и модификация не будет сохранена в БД; Беззвучно без предупреждения.


ИЗМЕНИТЬ 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();
}

Все это работало без проблем.


Популярные ответы

Насколько я знаю, не существует поддержки для повторной привязки измененных графиков (например, метод SaveOrUpdate nHibernate). Возможно , это и это может помочь вам.




Лицензировано согласно: CC-BY-SA with attribution
Не связан с Stack Overflow
Является ли этот КБ законным? Да, узнайте, почему
Лицензировано согласно: CC-BY-SA with attribution
Не связан с Stack Overflow
Является ли этот КБ законным? Да, узнайте, почему