由于一个或多个外键属性是非空的,所以无法更改关系。

217
我在对实体进行GetById()操作后,将子实体的集合设置为来自MVC视图的新列表时,遇到了这个错误。

操作失败:由于一个或多个外键属性是非空的,因此无法更改关系。当对关系进行更改时,相关的外键属性被设置为null值。如果外键不支持null值,则必须定义一个新的关系,将外键属性分配给另一个非null值,或者删除无关的对象。

我不太理解这一行:

由于一个或多个外键属性是非空的,所以无法更改关系。

为什么我要更改两个实体之间的关系?它应该在整个应用程序的生命周期内保持不变。

异常发生的代码很简单,只是将修改后的子类分配给现有父类的集合。这样做可以处理删除子类、添加新子类和修改子类的情况。我本以为Entity Framework会处理这个问题。

这些代码可以简化为:

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

我在下面的文章中使用了解决方案#2找到了答案,基本上是为了引用父表而在子表中创建了一个主键(因此它有2个主键(父表的外键和子表的ID)。https://www.c-sharpcorner.com/UploadFile/ff2f08/entity-framework-error-the-relationship-could-not-be-chang/ - yougotiger
@jaffa,我在这里找到了答案:https://stackoverflow.com/questions/22858491/entity-framework-remove-object-with-foreign-key-preserving-parent。 - antonio
1
对我来说,解决方法很简单。我的数据库外键列是可空整数,但我的 EF 属性是整数。我将其更改为 int? 以匹配数据库,问题得到解决。 - redwards510
21个回答

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个),这没问题。 希望能帮到你。

重新插入时,会使用新的ID还是保留子项最初的ID? - Pepito Fernandez

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(); - Andy Edinborough

1

我尝试了这些解决方案和许多其他方案,但它们都没有完全奏效。由于这是谷歌上的第一个答案,所以我会在这里添加我的解决方案。

对我有效的方法是在提交时将关系剥离,这样 EF 就没有机会弄乱。我通过重新在 DBContext 中查找父对象并删除它来实现这一点。由于重新查找的对象的导航属性全部为空,因此在提交期间会忽略子项的关系。

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

请注意,这假定外键已设置为ON DELETE CASCADE,因此当父行被删除时,数据库将清理子行。

1
我用了 Mosh的解决方案,但对于如何在代码优先模式下正确实现组合键并不明显。
因此,这是解决方案:
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
如果您正在使用Auto mapper并遇到以下问题,则以下是一个好的解决方案,这对我很有效。

https://www.codeproject.com/Articles/576393/Solutionplusto-aplus-Theplusoperationplusfailed

由于问题在于我们正在映射空的导航属性,而实际上我们不需要在实体上更新它们,因为它们在合同上没有改变,所以我们需要在映射定义中忽略它们:

ForMember(dest => dest.RefundType, opt => opt.Ignore())

所以我的代码最终变成了这样:
Mapper.CreateMap<MyDataContract, MyEntity>
ForMember(dest => dest.NavigationProperty1, opt => opt.Ignore())
ForMember(dest => dest.NavigationProperty2, opt => opt.Ignore())
.IgnoreAllNonExisting();

0

我也用Mosh的答案解决了我的问题,而且我认为PeterB的答案有点不太对,因为它将枚举用作外键。请记住,在添加此代码后,您需要添加新的迁移。

我还可以推荐这篇博客文章以获取其他解决方案:

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

当我尝试修改目标实体的标量属性时,遇到了同样的问题,后来发现我不小心引用了目标实体的父级:

entity.GetDbContextFromEntity().Entry(entity).Reference(i => i.ParentEntity).Query().Where(p => p.ID == 1).Load();

提供一个建议,确保目标实体不引用任何父级。


0
这个问题的出现是因为我们试图删除父表,但子表数据仍然存在。 我们通过级联删除的帮助来解决这个问题。
在DbContext类中的Create方法中。
 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();

级联删除选项使用这个简单的代码删除父表以及与其相关的子表。可以尝试这种简单的方法。

使用Remove Range删除数据库中的记录列表。谢谢。


0
当我尝试删除记录时,遇到了同样的问题,出现了一些问题。解决这个问题的方法是,在删除标题/主记录之前,您必须编写代码以删除其详细信息。我希望您的问题能够得到解决。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接