Як зупинити Entity Framework від спроб зберегти / вставити дочірні об’єкти?


100

Коли я зберігаю сутність за допомогою сутності фреймворка, я, природно, припускав, що вона намагатиметься лише зберегти вказану сутність. Однак він також намагається врятувати дочірні сутності цієї сутності. Це спричиняє всілякі проблеми з цілісністю. Як змусити EF зберігати лише сутність, яку я хочу зберегти, а отже ігнорувати всі дочірні об’єкти?

Якщо я вручну встановлюю для властивостей значення null, я отримую повідомлення про помилку "Операція не вдалася: зв’язок змінити не вдалося, оскільки одне або кілька властивостей зовнішнього ключа не мають нульового значення". Це надзвичайно контрпродуктивно, оскільки я спеціально встановив для дочірнього об'єкта значення null, щоб EF залишив його в спокої.

Чому я не хочу зберігати / вставляти дочірні об’єкти?

Оскільки це обговорюється туди-сюди в коментарях, я дам кілька обґрунтувань, чому я хочу, щоб мої дочірні предмети залишились самі.

У додатку, який я будую, об’єктна модель EF не завантажується з бази даних, а використовується як об’єкти даних, які я заповнюю під час аналізу плоского файлу. У випадку дочірніх об'єктів багато з них посилаються на таблиці пошуку, що визначають різні властивості батьківської таблиці. Наприклад, географічне розташування первинної сутності.

Оскільки я заповнив ці об'єкти сам, EF припускає, що це нові об'єкти, і їх потрібно вставити разом з батьківським об'єктом. Однак ці визначення вже існують, і я не хочу створювати дублікати в базі даних. Я використовую об'єкт EF лише для пошуку та заповнення зовнішнього ключа в моїй сутності основної таблиці.

Навіть з дочірніми об’єктами, які є справжніми даними, мені потрібно спочатку зберегти батьківського елемента та отримати первинний ключ або EF, здається, все робить безлад. Сподіваюся, це дає якесь пояснення.


Наскільки я знаю, вам доведеться нулювати дочірні об'єкти.
— Йохан

Привіт Йохане. Не працює. Він видає помилки, якщо я обнулюю колекцію. Залежно від того, як я це роблю, він скаржиться на те, що ключі є нульовими, або на те, що колекція моєї модифікована. Очевидно, що ці речі є правдою, але я зробив це навмисно, щоб це залишило в спокої предмети, до яких не слід торкатися.
— Mark Micallef

Ейфорія, це абсолютно безрезультатно.
— Mark Micallef

@Euphoric Навіть коли не змінюється дочірні об'єкти, EF все ще намагається вставити їх за замовчуванням, а не ігнорувати або оновлювати.
— Йохан

Мене насправді дратує те, що якщо я намагаюся реально обнулити ці об’єкти, то він скаржиться, а не розуміє, що хочу, щоб це просто залишило їх у спокої. Оскільки всі ці дочірні об'єкти є необов'язковими (в базі даних можна онулювати), чи є спосіб змусити EF забути, що у мене були ці об'єкти? тобто якось очистити його контекст чи кеш?
— Mark Micallef

Відповіді:


55

Наскільки я знаю, у вас є два варіанти.

Варіант 1)

Нуль всіх дочірніх об'єктів, це забезпечить EF нічого не додавати. Він також нічого не видалить з вашої бази даних.

Варіант 2)

Встановіть дочірні об'єкти як відірвані від контексту, використовуючи наступний код

 context.Entry(yourObject).State = EntityState.Detached

Зверніть увагу, що ви не можете від'єднати a List/ Collection. Вам доведеться перебирати ваш список і від’єднувати кожен елемент у вашому списку так

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}

Привіт Йохан, я спробував від'єднати одну з колекцій, і це призвело до наступної помилки: Тип сутності HashSet`1 не є частиною моделі для поточного контексту.
— Mark Micallef

@MarkyMark Не відривайте колекцію. Вам доведеться прокрутити колекцію та від’єднати її об’єкт від об’єкта (зараз я оновлю відповідь).
— Йохан

11
На жаль, навіть маючи список циклів, щоб від'єднати все, EF, схоже, все ще намагається вставити в деякі пов'язані таблиці. На даний момент я готовий вирвати EF і повернутися до SQL, який принаймні поводиться розумно. Який біль.
— Mark Micallef

2
Чи можу я використовувати context.Entry (yourObject) .State ще до того, як додати його?
— Thomas Klammer

1
@ mirind4 Я не використовував стан. Якщо я вставляю об’єкт, я переконуюсь, що всі дочірні елементи є нульовими. Після оновлення я отримую об'єкт спочатку без дочірніх елементів.
— Томас Кламмер

41

Коротко: Використовуйте зовнішній ключ, і це врятує ваш день.

Припустимо, що у вас є шкільний та міський утворення, і це стосунки багато до одного, коли в місті є багато шкіл, а школа належить місту. І припустімо, що міста вже існують у таблиці пошуку, тому ви НЕ хочете, щоб їх вставляли знову, коли вставляєте нову школу.

Спочатку ви можете визначити такі сутності:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

І ви можете зробити вставку школи таким чином (припустимо, у вас вже є властивість City, призначена newItem ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

Вищезазначений підхід може цілком спрацювати у цьому випадку, однак, я віддаю перевагу підходу зовнішнього ключа, який для мене є більш чітким та гнучким. Дивіться оновлене рішення нижче:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

Таким чином, ви чітко визначаєте, що школа має зовнішній ключ City_Id, і він посилається на сутність міста . Отже, коли справа доходить до включення школи , ви можете зробити:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

У цьому випадку ви явно вказуєте City_Id нового запису і видаляєте City з графіка, щоб EF не турбувався додавати його до контексту разом із School .

Хоча на перше враження підхід до зовнішнього ключа здається складнішим, але, повірте, ця ментальність заощадить вам багато часу, коли справа доходить до встановлення відносин багато-до-багатьох (уявляючи, що у вас є стосунки між школою та студентом, а студент має власність міста) тощо.

Сподіваюся, це вам корисно.


Чудова відповідь, мені дуже допомогло! Але чи не було б краще вказати мінімальне значення 1 для City_Id, використовуючи [Range(1, int.MaxValue)]атрибут?
— Ден Рейсон

Як мрія !! Завдяки мільйонів!
— CJH

Це призведе до видалення значення з об’єкта School у абонента. SaveChanges перезавантажує значення нульових властивостей навігації? В іншому випадку абонент повинен перезавантажити об'єкт City після виклику методу Insert (), якщо йому потрібна ця інформація. Це шаблон, який я часто використовував, але я все ще відкритий для кращих шаблонів, якщо хтось має хороший.
— однобокий

1
Це було мені дуже корисно. EntityState.Unchaged - це саме те, що мені потрібно було призначити об’єкту, який представляє зовнішній ключ таблиці пошуку у великому графіку об’єкта, який я зберігав як одну транзакцію. EF Core видає менш інтуїтивне повідомлення про помилку IMO. Я кешував свої пошукові таблиці, які рідко змінюються з міркувань продуктивності. Ви припускаєте, що оскільки ПК об'єкта підстановки таблиці такий самий, що і в базі даних, вже знає, що він незмінний, і просто призначити FK існуючому елементу. Замість спроб вставити об'єкт таблиці пошуку як новий.
— tnk479

Жодна комбінація нулювання або встановлення стану незмінним для мене не працює. EF наполягає, що йому потрібно вставити новий дочірній запис, коли я просто хочу оновити скалярне поле в батьківському.
— BobRz

21

Якщо ви просто хочете зберегти зміни в батьківському об’єкті та уникати зберігання змін у будь-якому з його дочірніх об’єктів, то чому б просто не зробити наступне:

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

Перший рядок приєднує батьківський об'єкт і весь графік залежних від нього дочірніх об'єктів до контексту у Unchangedстані.

Другий рядок змінює стан лише для батьківського об’єкта, залишаючи його дочірні елементи у Unchangedстані.

Зверніть увагу, що я використовую нещодавно створений контекст, тому це дозволяє уникнути збереження будь-яких інших змін у базі даних.


2
Ця відповідь має ту перевагу, що якщо хтось прийде пізніше і додасть дочірній об’єкт, він не порушить існуючий код. Це рішення "прийняти", де інші вимагають від вас явного виключення дочірніх об'єктів.
— Jim

Це більше не працює в основному. "Вкласти: Приєднує кожну доступну сутність, за винятком випадків, коли у досяжної сутності є ключ, створений сховищем, і значення ключа не призначено; вони будуть позначені як додані." Якщо діти також нові, вони будуть додані.
— mmix

14

Одним із запропонованих рішень є призначення властивості навігації з того самого контексту бази даних. У цьому рішенні властивість навігації, призначена поза контекстом бази даних, буде замінена. Будь ласка, див. Наступний приклад для ілюстрації.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

Збереження в базі даних:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft залучає це як функцію, однак мене це дратує. Якщо об’єкт відділу, пов’язаний з об’єктом компанії, має ідентифікатор, який вже існує в базі даних, то чому EF просто не пов’язує об’єкт компанії з об’єктом бази даних? Чому нам слід дбати про асоціацію самі? Турбота про властивість навігації під час додавання нового об’єкту - це щось на зразок переміщення операцій з базою даних із SQL на C #, що є громіздким для розробників.


Я згоден на 100% смішно, що EF намагається створити новий запис при наданні посвідчення особи. Якби існував спосіб заповнити параметри вибраного списку фактичним об’єктом, ми б займалися бізнесом.
— T3.0

10

Спочатку потрібно знати, що існує два шляхи оновлення сутності в EF.

  • Прикріплені предмети

Коли ви змінюєте взаємозв'язок об'єктів, приєднаних до контексту об'єкта, використовуючи один із описаних вище методів, Entity Framework повинен синхронізувати зовнішні ключі, посилання та колекції.

  • Відключені об'єкти

Якщо ви працюєте з відключеними об'єктами, вам слід вручну керувати синхронізацією.

У додатку, який я будую, об’єктна модель EF не завантажується з бази даних, а використовується як об’єкти даних, які я заповнюю під час аналізу плоского файлу.

Це означає, що ви працюєте з від’єднаним об’єктом, але незрозуміло, використовуєте ви незалежну асоціацію чи асоціацію із зовнішнім ключем .

  • Додайте

    При додаванні нової сутності з існуючим дочірнім об'єктом (об'єктом, який існує в базі даних), якщо дочірній об'єкт не відстежується EF, дочірній об'єкт буде повторно вставлений. Якщо спочатку не прикріпити дочірній об’єкт вручну.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Оновлення

    Ви можете просто позначити сутність як змінену, тоді всі скалярні властивості будуть оновлені, а властивості навігації будуть просто проігноровані.

      db.Entity(entity).State = EntityState.Modified;

Графік різниці

Якщо ви хочете спростити код під час роботи з відключеним об'єктом, ви можете спробувати побудувати графічну бібліотеку diff .

Ось вступ, представляючи GraphDiff для Entity Framework Code First - Дозвіл на автоматичне оновлення графіку відокремлених сутностей .

Зразок коду

  • Вставте сутність, якщо вона не існує, інакше оновіть.

      db.UpdateGraph(entity);
  • Вставте сутність, якщо вона не існує, інакше оновіть І вставте дочірній об’єкт, якщо вона не існує, інакше оновіть.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));

3

Найкращий спосіб це зробити, замінивши функцію SaveChanges у вашому контексті даних.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }

2

У мене така сама проблема, коли я намагаюся зберегти профіль, я вже вітаю таблицю та новий для створення профілю. Коли я вставляю профіль, він також вставляється у вітання. Тож я спробував так перед savechanges ().

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;


1

Це працювало для мене:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;

0

Що ми зробили, так це перед тим, як додати батьків до набору даних, від’єднати дочірні колекції від батьків, переконувавшись перенести існуючі колекції в інші змінні, щоб надалі працювати з ними, а потім замінивши поточні дочірні колекції новими порожніми колекціями. Встановлення для дочірніх колекцій значення null / нічого не здавалося для нас невдалим. Після цього додайте батьківського елемента до набору даних. Таким чином, дітей не додають, поки ви цього не захочете.


0

Я знаю, що це старий пост, однак, якщо ви використовуєте підхід першого коду, ви можете досягти бажаного результату, використовуючи наступний код у своєму файлі відображення.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Це в основному скаже EF, щоб виключити властивість "ChildObjectOrCollection" із моделі, щоб вона не була зіставлена ​​з базою даних.


У якому контексті використовується "Ігнорувати"? Здається, він не існує в передбачуваному контексті.
— T3.0

0

Я мав подібний виклик, використовуючи Entity Framework Core 3.1.0, моя логіка репо є досить загальною.

Це працювало для мене:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Зверніть увагу, "ParentEntityId" - це ім'я стовпця зовнішнього ключа на дочірній сутності. Я додав згаданий вище рядок коду за цим методом:

protected override void OnModelCreating(ModelBuilder builder)...
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.