Невозможно изменить отношение, так как одно или несколько свойств внешнего ключа не могут быть равны нулю - PullRequest
172 голосов
/ 04 апреля 2011

Я получаю эту ошибку, когда я получаю GetById () для сущности, а затем задаю для коллекции дочерних сущностей мой новый список, который поступает из представления MVC.не может быть изменено, потому что одно или несколько свойств внешнего ключа не могут иметь значение NULL.Когда в отношение вносится изменение, для соответствующего свойства внешнего ключа устанавливается нулевое значение.Если внешний ключ не поддерживает нулевые значения, необходимо определить новое отношение, свойству внешнего ключа должно быть назначено другое ненулевое значение или несвязанный объект должен быть удален.

Iне совсем понимаю эту строку:

Отношение не может быть изменено, поскольку одно или несколько свойств внешнего ключа не могут быть обнуляемыми.изменить отношения между двумя организациями?Он должен оставаться неизменным на протяжении всего жизненного цикла всего приложения.

Код, в котором возникает исключение, заключается в простом присвоении измененных дочерних классов в коллекции существующему родительскому классу.Надеемся, что это поможет удалить дочерние классы, добавить новые и модификации.Я бы подумал, что Entity Framework справится с этим.

Строки кода можно перевести в:

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

Ответы [ 18 ]

149 голосов
/ 04 апреля 2011

Вы должны удалить старые дочерние элементы 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, а затем код выглядит немного иначе.) Я также убрал все абстракции репозитория, чтобы упростить его.

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

96 голосов
/ 07 октября 2015

Причина, по которой вы столкнулись с этим, связана с разницей между состав и агрегация .

В композиции дочерний объект создается при создании родителя и уничтожается при уничтожении его родителя . Таким образом, его время жизни контролируется его родителем. например Сообщение в блоге и его комментарии. Если сообщение удалено, его комментарии должны быть удалены. Не имеет смысла оставлять комментарии к несуществующему сообщению. То же самое для заказов и элементов заказа.

В агрегации дочерний объект может существовать независимо от его родителя . Если родитель уничтожен, дочерний объект все еще может существовать, так как он может быть добавлен к другому родителю позже. например: связь между списком воспроизведения и песнями в этом списке воспроизведения. Если список воспроизведения удален, песни не должны быть удалены. Они могут быть добавлены в другой список воспроизведения.

Способ, которым Entity Framework различает отношения агрегации и композиции, выглядит следующим образом:

  • Для композиции: ожидается, что дочерний объект будет иметь составной первичный ключ (ParentID, ChildID). Это сделано специально, так как идентификаторы детей должны быть в пределах их родителей.

  • Для агрегации: ожидается, что свойство внешнего ключа в дочернем объекте будет иметь значение null.

Итак, причина, по которой вы столкнулись с этой проблемой, заключается в том, как вы установили свой первичный ключ в своей дочерней таблице. Он должен быть составным, но это не так. Таким образом, Entity Framework рассматривает эту ассоциацию как агрегацию, что означает, что при удалении или очистке дочерних объектов дочерние записи не удаляются. Он просто удалит связь и установит для соответствующего столбца внешнего ключа значение NULL (чтобы эти дочерние записи впоследствии могли быть связаны с другим родителем). Поскольку ваш столбец не допускает NULL, вы получите исключение, которое вы упомянули.

Решения:

1 - Если у вас есть веская причина не использовать составной ключ, вам необходимо явно удалить дочерние объекты. И это можно сделать проще, чем предложенные ранее решения:

context.Children.RemoveRange(parent.Children);

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

parent.Children.Clear();
68 голосов
/ 04 апреля 2011

Это очень большая проблема.То, что на самом деле происходит в вашем коде, таково:

  • Вы загружаете 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();
38 голосов
/ 01 апреля 2014

Я нашел этот ответ гораздо более полезным для той же ошибки. Кажется, что EF не нравится, когда вы удаляете, он предпочитает Удалить.

Вы можете удалить коллекцию записей, прикрепленных к такой записи.

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

В этом примере для всех подробных записей, прикрепленных к ордеру, установлено состояние «Удалить». (При подготовке к добавлению обратно обновленных сведений в рамках обновления заказа)

19 голосов
/ 22 апреля 2013

Понятия не имею, почему два других ответа так популярны!

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

Вы должны определить родителя- дочерние отношения правильно, используя « идентифицирующее отношение ».

Если вы сделаете это, Entity Framework узнает, что дочерний объект идентифицирован родителем,и, следовательно, это должна быть ситуация «каскадного удаления-сироты».

Помимо вышесказанного, вам может понадобиться (из опыта NHibernate)

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

вместо полной замены списка.

ОБНОВЛЕНИЕ

@ Комментарий Слаумы напомнил мне, что отсоединенные объекты являются еще одной частью общей проблемы.Чтобы решить эту проблему, вы можете использовать подход связывания пользовательских моделей, который создает ваши модели, пытаясь загрузить его из контекста. Это сообщение в блоге показывает пример того, что я имею в виду.

9 голосов
/ 17 февраля 2016

Если вы используете 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;, это вызовет проблему.

Установите в свой AutoMapper значение Игнорировать ClassB.

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
4 голосов
/ 25 августа 2015

Это происходит потому, что дочерний объект помечается как измененный, а не как удаленный.

И модификация, которую EF выполняет с дочерним объектом при выполнении parent.Remove(child), просто устанавливает ссылку на его родительский элемент равным null.

Вы можете проверить EntityState ребенка, введя следующий код в Immediate Window 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 правильно пометит сущность как удаленную.

3 голосов
/ 05 августа 2015

У меня просто была такая же ошибка. У меня есть две таблицы с родительскими и дочерними отношениями, но я настроил «каскад удаления» для столбца внешнего ключа в определении таблицы дочерней таблицы. Поэтому, когда я вручную удаляю родительскую строку (через SQL) в базе данных, она автоматически удаляет дочерние строки.

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

Я вручную изменил параметр End1 OnDelete на Cascade и чем это сработало. Я не знаю, почему EF не может поднять это, когда я обновляю модель из базы данных (у меня есть база данных первой модели).

Для полноты, вот как выглядит мой код для удаления:

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

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

Если бы я не определил каскадное удаление, мне пришлось бы вручную удалить дочерние строки перед удалением родительской строки.

2 голосов
/ 22 сентября 2015

Необходимо вручную очистить коллекцию 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);
    }
}
2 голосов
/ 23 июня 2015

Я столкнулся с этой проблемой сегодня и хотел поделиться своим решением.В моем случае решение состояло в том, чтобы удалить дочерние элементы перед получением родительского элемента из базы данных.

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

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

Что мне помогло, так это сначала получить дочерние элементы, используя parentId (внешний ключ), а затем удалить эти элементы.Затем я могу получить Parent из базы данных, и в этот момент у него больше не должно быть дочерних элементов, и я могу добавлять новые дочерние элементы.

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...