The relationship could not be changed because one or more of the foreign-key properties is non-nullable

前端 未结 20 1052
情书的邮戳
情书的邮戳 2020-11-22 04:44

I am getting this error when I GetById() on an entity and then set the collection of child entities to my new list which comes from the MVC view.

The

相关标签:
20条回答
  • 2020-11-22 05:07

    I've tried these solutions and many others, but none of them quite worked out. Since this is the first answer on google, I'll add my solution here.

    The method that worked well for me was to take relationships out of the picture during commits, so there was nothing for EF to screw up. I did this by re-finding the parent object in the DBContext, and deleting that. Since the re-found object's navigation properties are all null, the childrens' relationships are ignored during the commit.

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

    Note that this assumes the foreign keys are setup with ON DELETE CASCADE, so when the parent row is removed, the children will be cleaned up by the database.

    0 讨论(0)
  • 2020-11-22 05:09

    This is a very big problem. What actually happens in your code is this:

    • You load Parent from the database and get an attached entity
    • You replace its child collection with new collection of detached children
    • You save changes but during this operation all children are considered as added becasue EF didn't know about them till this time. So EF tries to set null to foreign key of old children and insert all new children => duplicate rows.

    Now the solution really depends on what you want to do and how would you like to do it?

    If you are using ASP.NET MVC you can try to use UpdateModel or TryUpdateModel.

    If you want just update existing children manually, you can simply do something like:

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

    Attaching is actually not needed (setting the state to Modified will also attach the entity) but I like it because it makes the process more obvious.

    If you want to modify existing, delete existing and insert new childs you must do something like:

    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();
    
    0 讨论(0)
  • 2020-11-22 05:10

    I ran into this problem today and wanted to share my solution. In my case, the solution was to delete the Child items before getting the Parent from the database.

    Previously I was doing it like in the code below. I will then get the same error listed in this question.

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

    What worked for me, is to get the children items first, using the parentId (foreign key) and then delete those items. Then I can get the Parent from the database and at that point, it should not have any children items anymore and I can add new children items.

    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
    
    0 讨论(0)
  • 2020-11-22 05:11

    You should delete old child items thisParent.ChildItems one by one manually. Entity Framework doesn't do that for you. It finally cannot decide what you want to do with the old child items - if you want to throw them away or if you want to keep and assign them to other parent entities. You must tell Entity Framework your decision. But one of these two decisions you HAVE to make since the child entities cannot live alone without a reference to any parent in the database (due to the foreign key constraint). That's basically what the exception says.

    Edit

    What I would do if child items could be added, updated and deleted:

    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();
    }
    

    Note: This is not tested. It's assuming that the child item collection is of type ICollection. (I usually have IList and then the code looks a bit different.) I've also stripped away all repository abstractions to keep it simple.

    I don't know if that is a good solution, but I believe that some kind of hard work along these lines must be done to take care of all kinds of changes in the navigation collection. I would also be happy to see an easier way of doing it.

    0 讨论(0)
  • 2020-11-22 05:12

    You must manually clear the ChildItems collection and append new items into it:

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

    After that you can call DeleteOrphans extension method which will handle with orphaned entities (it must be called between DetectChanges and SaveChanges methods).

    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);
        }
    }
    
    0 讨论(0)
  • 2020-11-22 05:12

    I was face same problem when I am going to delete my record than some issue was occur , for this issue solution is that when you are going to delete your record than you missing some thing before deleting header/master record you must write to code for delete its detail before header/Master I hope you issue will be resolve.

    0 讨论(0)
提交回复
热议问题