Відносини неможливо змінити, оскільки одна або декілька властивостей іншомовного ключа не зведені до нуля


192

Я отримую цю помилку, коли отримую GetById () на об'єкті, а потім встановлюю колекцію дочірніх об'єктів до мого нового списку, який надходить із подання MVC.

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

Я не зовсім розумію цю лінію:

Відносини неможливо змінити, оскільки одна або декілька властивостей іншомовного ключа не зведені до нуля.

Чому я міняв би відносини між двома сутностями? Він повинен залишатися однаковим протягом усього життя всієї програми.

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

Рядки коду можна перегнати:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();

Я знайшов свою відповідь купити за допомогою рішення №2 у нижченаведеній статті, я в основному створив доданий первинний ключ до дочірньої таблиці для посилання на батьківську таблицю (тому у неї є 2 первинних ключа (зовнішній ключ для батьківської таблиці та ідентифікатор) для дитячого столу). c-sharpcorner.com/UploadFile/ff2f08/…
yougotiger

@jaffa, я знайшов свою відповідь тут stackoverflow.com/questions/22858491/…
antonio

Відповіді:


159

Ви повинні видаляти старі дочірні елементи thisParent.ChildItemsодин за одним вручну. Entity Framework не робить цього для вас. Зрештою, він не може вирішити, що ви хочете робити зі старими дочірніми предметами - якщо ви хочете їх викинути або якщо ви хочете зберегти та призначити їх іншим материнським організаціям. Ви повинні повідомити Entity Framework своє рішення. Але одне з цих двох рішень ви мали прийняти, оскільки дочірні організації не можуть жити наодинці без посилання на жодного з батьків у базі даних (через обмеження закордонних ключів). Це в основному те, що говорить виняток.

Редагувати

Що б я зробив, якщо дочірні елементи можна було б додавати, оновлювати та видаляти:

public void UpdateEntity(ParentItem parent)
{
    // Load original parent including the child item collection
    var originalParent = _dbContext.ParentItems
        .Where(p => p.ID == parent.ID)
        .Include(p => p.ChildItems)
        .SingleOrDefault();
    // We assume that the parent is still in the DB and don't check for null

    // Update scalar properties of parent,
    // can be omitted if we don't expect changes of the scalar properties
    var parentEntry = _dbContext.Entry(originalParent);
    parentEntry.CurrentValues.SetValues(parent);

    foreach (var childItem in parent.ChildItems)
    {
        var originalChildItem = originalParent.ChildItems
            .Where(c => c.ID == childItem.ID && c.ID != 0)
            .SingleOrDefault();
        // Is original child item with same ID in DB?
        if (originalChildItem != null)
        {
            // Yes -> Update scalar properties of child item
            var childEntry = _dbContext.Entry(originalChildItem);
            childEntry.CurrentValues.SetValues(childItem);
        }
        else
        {
            // No -> It's a new child item -> Insert
            childItem.ID = 0;
            originalParent.ChildItems.Add(childItem);
        }
    }

    // Don't consider the child items we have just added above.
    // (We need to make a copy of the list by using .ToList() because
    // _dbContext.ChildItems.Remove in this loop does not only delete
    // from the context but also from the child collection. Without making
    // the copy we would modify the collection we are just interating
    // through - which is forbidden and would lead to an exception.)
    foreach (var originalChildItem in
                 originalParent.ChildItems.Where(c => c.ID != 0).ToList())
    {
        // Are there child items in the DB which are NOT in the
        // new child item collection anymore?
        if (!parent.ChildItems.Any(c => c.ID == originalChildItem.ID))
            // Yes -> It's a deleted child item -> Delete
            _dbContext.ChildItems.Remove(originalChildItem);
    }

    _dbContext.SaveChanges();
}

Примітка. Це не перевірено. Це припущення, що дочірня колекція предметів має тип ICollection. (У мене зазвичай є, IListі тоді код виглядає дещо інакше.) Я також позбавив усіх абстракцій сховища, щоб зробити це просто.

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


То що робити, якщо деякі лише змінені? Це означає, що я все одно повинен їх видалити та додати знову?
jaffa

@Jon: Ні, ви також можете оновити наявні елементи. Я додав приклад того, як я, мабуть, оновив дочірню колекцію, див. Розділ Редагування вище.
Слаума

@Slauma: Лол, якби я знав, що ти збираєшся змінити свою відповідь, я б не написав своєї відповіді ...
Ladislav Mrnka

@Ladislav: Ні, ні, я радий, що ти написав власну відповідь. Тепер принаймні я знаю, що це не повна нісенітниця і набагато надто складна те, що я робив вище.
Слаума

1
Я б додав умову під час отримання originalChildItem в передбаченні: ... де (c => c.ID == childItem.ID && c.ID! = 0) в іншому випадку він поверне щойно доданих дітей, якщо childItem.ID == 0.
perfect_element

116

Причина, з якою ви стикаєтесь, пов’язана з різницею між складом та агрегацією .

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

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

Те, як Entity Framework розмежовує зв'язки сукупності та складу, полягає в наступному:

  • За складом: він очікує, що дочірній об’єкт матиме складений первинний ключ (ParentID, ChildID). Це задумано, оскільки посвідчення дітей повинні бути в межах їхніх батьків.

  • Для агрегації: він очікує, що властивість стороннього ключа в дочірньому об'єкті буде зведеною нанівець.

Отже, причина, з якою виникає ця проблема, полягає в тому, як ви встановили свій основний ключ у своїй дочірній таблиці. Він повинен бути складеним, але це не так. Отже, Entity Framework розглядає цю асоціацію як агрегацію, що означає, що коли ви видаляєте або очищаєте дочірні об’єкти, вона не збирається видаляти дочірні записи. Він просто видалить асоціацію та встановить відповідний стовпчик із іноземним ключем NULL (щоб ці дочірні записи згодом могли бути пов’язані з іншим батьківським). Оскільки ваш стовпець не дозволяє NULL, ви отримуєте згадане вами виняток.

Рішення:

1- Якщо у вас є вагомі причини не бажати використовувати складений ключ, вам потрібно видалити дочірні об’єкти чітко. І це можна зробити простіше, ніж рішення, запропоновані раніше:

context.Children.RemoveRange(parent.Children);

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

parent.Children.Clear();

9
Я вважав це пояснення найбільш корисним.
Booji Boy

7
Хороше пояснення щодо складу та агрегації та того, як структура сутності стосується цього.
Хризаліс

№1 була найменшою кількістю коду, необхідної для усунення проблеми. Дякую!
ryanulit

73

Це дуже велика проблема. Що насправді відбувається у вашому коді:

  • Ви завантажуєте Parentз бази даних і отримуєте додану сутність
  • Ви замінюєте його дитячу колекцію новою колекцією відсторонених дітей
  • Ви зберігаєте зміни, але під час цієї операції всі діти вважаються доданими, оскільки EF до цього часу не знав про них. Тож EF намагається встановити нуль для іноземного ключа старих дітей і вставити всі нові діти => повторювані рядки.

Тепер рішення дійсно залежить від того, що ви хочете зробити і як би ви хотіли це зробити?

Якщо ви використовуєте ASP.NET MVC, ви можете спробувати скористатися UpdateModel або TryUpdateModel .

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

foreach (var child in modifiedParent.ChildItems)
{
    context.Childs.Attach(child); 
    context.Entry(child).State = EntityState.Modified;
}

context.SaveChanges();

Вкладення насправді не потрібне (встановлення стану Modifiedтакож додаватиме сутність), але мені це подобається, оскільки це робить процес більш очевидним.

Якщо ви хочете змінити існуючі, видалити існуючі та вставити нових дітей, ви повинні зробити щось на кшталт:

var parent = context.Parents.GetById(1); // Make sure that childs are loaded as well
foreach(var child in modifiedParent.ChildItems)
{
    var attachedChild = FindChild(parent, child.Id);
    if (attachedChild != null)
    {
        // Existing child - apply new values
        context.Entry(attachedChild).CurrentValues.SetValues(child);
    }
    else
    {
        // New child
        // Don't insert original object. It will attach whole detached graph
        parent.ChildItems.Add(child.Clone());
    }
}

// Now you must delete all entities present in parent.ChildItems but missing
// in modifiedParent.ChildItems
// ToList should make copy of the collection because we can't modify collection
// iterated by foreach
foreach(var child in parent.ChildItems.ToList())
{
    var detachedChild = FindChild(modifiedParent, child.Id);
    if (detachedChild == null)
    {
        parent.ChildItems.Remove(child);
        context.Childs.Remove(child); 
    }
}

context.SaveChanges();

1
Але є ваше цікаве зауваження щодо використання .Clone(). Чи маєте ви на увазі випадок, що a ChildItemмає інші навігаційні властивості під-дочірніх? Але в цьому випадку, чи не хочемо ми, щоб весь під графік був приєднаний до контексту, оскільки ми могли б очікувати, що всі піддіті - це нові об'єкти, якщо сама дитина нова? (Ну, може, відрізняється від моделі до моделі, але припустимо, випадок, що
піддітні діти

Напевно, знадобиться "розумний" клон.
Ladislav Mrnka

1
Що робити, якщо ви не хочете мати у своєму контексті дитячу колекцію? http://stackoverflow.com/questions/20233994/do-i-need-to-create-a-dbset-for-every-table-so-that-i-can-persist-child-entitie
Кірстен Жадібний

1
parent.ChildItems.Remove (дитина); контекст.Діти.Вдалити (дитина); Ця подвійна помилка вилучення може вийти, ДЯКУЄМО Навіщо нам потрібно обидва видалення? Чому вилучати лише з батьків.ChildItems не зближує, оскільки діти живуть лише дітьми?
Фернандо Торрес

40

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

Ви можете видалити колекцію записів, приєднану до такої записи.

order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);

У прикладі всіх записів деталей, що додаються до замовлення, встановлено стан Видалити. (Готуючись додати назад оновлені реквізити, як частину оновлення замовлення)


Я вважаю, що це правильна відповідь.
десмати

логічне та прямолінійне рішення.
sairfan

19

Я не знаю, чому інші два відповіді такі популярні!

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

Ви повинні правильно визначити відносини батько - дитина , використовуючи " ототожнюючі відносини ".

Якщо ви це зробите, Entity Framework знає, що дочірній об’єкт ідентифікований батьком, і тому це має бути ситуація "каскад-видалення-сироти".

Крім вищезазначеного, вам може знадобитися (із досвіду NHibernate)

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

замість того, щоб повністю замінювати список.

ОНОВЛЕННЯ

@ Коментар Слауми нагадав, що відокремлені об'єкти - це ще одна частина загальної проблеми. Щоб вирішити це, ви можете скористатися підходом до використання в'яжучої моделі на замовлення, яка будує ваші моделі, намагаючись завантажити його з контексту. У цій публікації в блозі показаний приклад того, що я маю на увазі.


Налаштування як виявлення взаємозв'язку тут не допоможе, оскільки сценарій у питанні має стосуватися відокремлених об'єктів ( "мій новий список, який надходить з перегляду MVC" ). Вам залишається завантажити оригінальні діти з БД, знайти видалені елементи в цій колекції на основі відокремленої колекції, а потім видалити з БД. Єдина відмінність полягає в тому, що з ідентифікаційними відносинами ви можете дзвонити parent.ChildItems.Removeзамість _dbContext.ChildItems.Remove. Досі (EF <= 6) немає вбудованої підтримки від EF, щоб уникнути тривалого коду, як той, що в інших відповідях.
Слаума

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

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

Спрощенням є автоматичне видалення осиротілих осіб. Все, що вам потрібно в моделі палітурки, - це загальний еквівалентreturn context.Items.Find(id) ?? new Item()
Андре Луус

Хороший відгук для команди EF, але запропоноване рішення, на жаль, нічого не вирішує на землі EF.
Кріс Москіні

9

Якщо ви використовуєте AutoMapper з Entity Framework одного класу, ви можете зіткнутися з цією проблемою. Наприклад, якщо ваш клас

class A
{
    public ClassB ClassB { get; set; }
    public int ClassBId { get; set; }
}

AutoMapper.Map<A, A>(input, destination);

Це спробує скопіювати обидва властивості. У цьому випадку ClassBId не є нульовим. Оскільки AutoMapper скопіює destination.ClassB = input.ClassB;це, це спричинить проблему.

Встановіть ClassBвластивість AutoMapper на "Ігнорувати" .

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId

Я зіткнувся з подібною проблемою з AutoMapper, але це не працює для мене :( Дивіться stackoverflow.com/q/41430679/613605
J86,

4

У мене просто була така ж помилка. У мене є дві таблиці з родительськими дочірніми стосунками, але я налаштував "каскад видалення" на стовпчику іноземних ключів у таблиці визначення дочірньої таблиці. Тож коли я вручну видалю батьківський рядок (через SQL) у базі даних, він автоматично видалить дочірні рядки.

Однак це не спрацювало в EF, виявилася помилка, описана в цій темі. Причиною цього було те, що в моїй моделі даних сутності (файл edmx) властивості асоціації між батьківською та дочірньою таблицею були невірними. End1 OnDeleteВаріант був налаштований бути none( «END1» в моїй моделі кінець якої має множинність 1).

Я вручну змінив End1 OnDeleteпараметр на Cascadeі чим він працював. Я не знаю, чому EF не в змозі підібрати це, коли я оновлюю модель з бази даних (у мене перша модель бази даних).

Для повноти так виглядає мій код для видалення:

   public void Delete(int id)
    {
        MyType myObject = _context.MyTypes.Find(id);

        _context.MyTypes.Remove(myObject);
        _context.SaveChanges(); 
   }    

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


4

Це трапляється тому, що дочірня особа позначена як Змінена, а не Видалена.

І модифікація, яку EF робить для дочірньої сутності, коли parent.Remove(child)виконується, просто встановлює посилання на свого батьківського null.

Ви можете перевірити EntityState дитини, ввівши наступний код у Негайне вікно Visual Studio, коли відбувається виняток, після виконання SaveChanges():

_context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity

де X слід замінити видаленою суттю.

Якщо у вас немає доступу до ObjectContextзапуску _context.ChildEntity.Remove(child), ви можете вирішити цю проблему, зробивши зовнішній ключ частиною первинного ключа на дочірньому столі.

Parent
 ________________
| PK    IdParent |
|       Name     |
|________________|

Child
 ________________
| PK    IdChild  |
| PK,FK IdParent |
|       Name     |
|________________|

Таким чином, якщо ви виконаєте parent.Remove(child), EF правильно позначить об'єкт видаленим.


2

Цей тип рішення зробив для мене хитрість:

Parent original = db.Parent.SingleOrDefault<Parent>(t => t.ID == updated.ID);
db.Childs.RemoveRange(original.Childs);
updated.Childs.ToList().ForEach(c => original.Childs.Add(c));
db.Entry<Parent>(original).CurrentValues.SetValues(updated);

Важливо сказати, що це видаляє всі записи та вставляє їх знову. Але для мого випадку (менше 10) це нормально.

Я сподіваюся, що це допомагає.


Чи відбувається повторне введення з новими посвідченнями особи чи це зберігає посвідчення дитини, які вони мали, в першу чергу?
Пепіто Фернандес

2

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

Раніше я робив це, як у наведеному нижче коді. Тоді я отримаю ту саму помилку, яку вказано в цьому запитанні.

var Parent = GetParent(parentId);
var children = Parent.Children;
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

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

var children = GetChildren(parentId);
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

var Parent = GetParent(parentId);
Parent.Children = //assign new entities/items here

2

Ви повинні очистити колекцію ChildItems вручну та додати до неї нові елементи:

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

Після цього ви можете зателефонувати методу розширення DeleteOrphans, який буде працювати з осиротілими об'єктами (його потрібно викликати між методами DetectChanges та SaveChanges).

public static class DbContextExtensions
{
    private static readonly ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>> s_navPropMappings = new ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>>();

    public static void DeleteOrphans( this DbContext source )
    {
        var context = ((IObjectContextAdapter)source).ObjectContext;
        foreach (var entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
        {
            var entityType = entry.EntitySet.ElementType as EntityType;
            if (entityType == null)
                continue;

            var navPropMap = s_navPropMappings.GetOrAdd(entityType, CreateNavigationPropertyMap);
            var props = entry.GetModifiedProperties().ToArray();
            foreach (var prop in props)
            {
                NavigationProperty navProp;
                if (!navPropMap.TryGetValue(prop, out navProp))
                    continue;

                var related = entry.RelationshipManager.GetRelatedEnd(navProp.RelationshipType.FullName, navProp.ToEndMember.Name);
                var enumerator = related.GetEnumerator();
                if (enumerator.MoveNext() && enumerator.Current != null)
                    continue;

                entry.Delete();
                break;
            }
        }
    }

    private static ReadOnlyDictionary<string, NavigationProperty> CreateNavigationPropertyMap( EntityType type )
    {
        var result = type.NavigationProperties
            .Where(v => v.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
            .Where(v => v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One || (v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne && v.FromEndMember.GetEntityType() == v.ToEndMember.GetEntityType()))
            .Select(v => new { NavigationProperty = v, DependentProperties = v.GetDependentProperties().Take(2).ToArray() })
            .Where(v => v.DependentProperties.Length == 1)
            .ToDictionary(v => v.DependentProperties[0].Name, v => v.NavigationProperty);

        return new ReadOnlyDictionary<string, NavigationProperty>(result);
    }
}

Це добре спрацювало для мене. Мені просто потрібно було додати context.DetectChanges();.
Енді Едінборо

1

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

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

var toDelete = db.Parents.Find(parentObject.ID);
db.Parents.Remove(toDelete);
db.SaveChanges();

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


1

Я використовував рішення Моша , але мені було очевидно, як спочатку правильно виконати ключ композиції в коді.

Тож ось рішення:

public class Holiday
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int HolidayId { get; set; }
    [Key, Column(Order = 1), ForeignKey("Location")]
    public LocationEnum LocationId { get; set; }

    public virtual Location Location { get; set; }

    public DateTime Date { get; set; }
    public string Name { get; set; }
}

1

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

parent.OtherRelatedItems.Clear();  //this worked OK on SaveChanges() - items were being deleted from DB
parent.ProblematicItems.Clear();   // this was causing the mentioned exception on SaveChanges()
  • OtherRelatedItems мав складений первинний ключ (parentId + деякий локальний стовпець) і працював у порядку
  • ProblematicItems мали власний одноколонний первинний ключ, а parentId - лише FK. Це спричинило виняток після Clear ().

Все, що я повинен був зробити, це зробити ParentId частиною складеної ПК, щоб вказати, що діти не можуть існувати без батька. Я використав модель DB-first, додав ПК та позначив стовпчик parentId як EntityKey (значить, мені довелося оновити його і в БД, і в EF - не впевнений, чи вистачить лише одного EF).

Я зробив RequestId частиною ПК А потім оновив модель EF та встановив іншу властивість як частину Entity Key

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


0

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

У моделі Створити метод у класі dbcontext.

 modelBuilder.Entity<Job>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                .WithRequired(C => C.Job)
                .HasForeignKey(C => C.JobId).WillCascadeOnDelete(true);
            modelBuilder.Entity<Sport>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                  .WithRequired(C => C.Sport)
                  .HasForeignKey(C => C.SportId).WillCascadeOnDelete(true);

Після цього в нашому API-дзвінку

var JobList = Context.Job                       
          .Include(x => x.JobSportsMappings)                                     .ToList();
Context.Job.RemoveRange(JobList);
Context.SaveChanges();

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

Видаліть діапазон, який використовувався для видалення списку записів у базі даних Дякую


0

Я також вирішив свою проблему з відповіддю Моша, і я вважав , що відповідь PeterB була дещо, оскільки вона використовувала enum як зовнішній ключ. Пам’ятайте, що вам потрібно буде додати нову міграцію після додавання цього коду.

Я також можу рекомендувати цю публікацію в блозі для інших рішень:

http://www.kianryan.co.uk/2013/03/orphaned-child/

Код:

public class Child
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Heading { get; set; }
    //Add other properties here.

    [Key, Column(Order = 1)]
    public int ParentId { get; set; }

    public virtual Parent Parent { get; set; }
}

0

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

Всі мої стійкі об’єкти реалізують цей інтерфейс

/// <summary>
/// Base interface for all persisted entries
/// </summary>
public interface IBase
{
    /// <summary>
    /// The Id
    /// </summary>
    int Id { get; set; }
}

Цим я реалізував ці дві функції у своєму сховищі

    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public T AddOrUpdateEntry<T>(DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.Id == 0 || orgEntry == null)
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            Context.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public void AddOrUpdateCollection<T>(DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }

Для його використання я виконую наступне:

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

Сподіваюся, це допомагає


ДОПОМОГА: Ви також можете зробити окремий клас DbContextExtentions (або власний інтерфейс контексту):

public static void DbContextExtentions {
    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public static T AddOrUpdateEntry<T>(this DbContext _dbContext, DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.IsNew || orgEntry == null) // New or not found in context
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            _dbContext.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public static void AddOrUpdateCollection<T>(this DbContext _dbContext, DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(_dbContext, set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }
}

і використовувати його так:

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = _dbContext.AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

Ви також можете скласти розширений клас для свого контексту за допомогою цих функцій:
Bluemoon74

0

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


-1

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

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

 public void CheckUsersCount(CompanyProduct companyProduct) 
 {
     companyProduct.Name = "Test";
 }

Використовуй це:

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.