Як зупинити 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.