该关系不能改变,因为一个或多个外键属性是不可空的

我得到这个错误,当我对一个实体的GetById(),然后将子实体的集合设置到我的新列表来自MVC视图。

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

我不太明白这一行:

该关系不能改变,因为一个或多个外键属性是不可空的。

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

发生exception的代码很简单,将集合中的修改过的子类分配给现有的父类。 这将有助于去除儿童class,增加新的和修改。 我会认为entity framework处理这个。

代码行可以被提炼为:

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

你应该手动删除旧的子项目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(); } 

注意:这没有testing。 假定子项目集合是ICollectiontypes的。 (我通常有IList ,然后代码看起来有点其他)。我也删除了所有的存储库抽象,以保持简单。

我不知道这是否是一个好的解决办法,但是我相信为了照顾导航集合中的各种变化,必须采取一些这方面的努力。 我也很高兴看到一个更简单的方法。

这是一个非常大的问题。 你的代码实际发生了什么:

  • 您从数据库中加载Parent并获取附加的实体
  • 你用新的独立的孩子收集它的儿童collections
  • 您保存更改,但在此操作过程中,所有的孩子都被视为已添加,因为EF直到此时才知道他们。 因此,EF尝试将空值设置为旧的外键,并插入所有新的孩子=>重复的行。

现在解决scheme真的取决于你想要做什么,你想怎么做?

如果您使用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(); 

你面对这个的原因是由于构图聚合的区别。

在构图中,子对象是在创build父对象时创build的,并且在其父对象被销毁时被销毁 。 所以它的寿命是由它的父母控制的。 例如一篇博客文章及其评论。 如果一个post被删除,其评论应该被删除。 对于不存在的post发表评论是没有意义的。 相同的订单和订单项目。

在聚合中,子对象可以存在,而不pipe其父对象 。 如果父项被销毁,那么子对象仍然可以存在,因为它可能稍后被添加到不同的父项中。 例如:播放列表与该播放列表中的歌曲之间的关系。 如果播放列表被删除,歌曲不应该被删除。 他们可能被添加到不同的播放列表。

entity framework区分聚合和组合关系的方式如下:

  • 对于组合:它期望子对象有一个复合主键(ParentID,ChildID)。 这是在devise上,因为孩子的身份证应该在父母的范围之内。

  • 对于聚合:它期望子对象中的外键属性可以为空。

所以,你有这个问题的原因是因为你如何设置你的子表中的主键。 它应该是复合的,但不是。 所以,Entity Framework把这个关联视为聚合,这意味着当你删除或清除子对象时,它不会删除子logging。 它只会删除关联,并将相应的外键列设置为NULL(以便这些子logging稍后可以与不同的父项关联)。 由于你的列不允许NULL,你会得到你提到的exception。

解决scheme:

1-如果你有一个强烈的理由不想使用组合键,你需要显式删除子对象。 这可以比前面提出的解决scheme更简单:

 context.Children.RemoveRange(parent.Children); 

2-否则,通过在你的子表上设置正确的主键,你的代码将看起来更有意义:

 parent.Children.Clear(); 

我发现这个答案对同样的错误更有帮助。 EF似乎不喜欢它,当你删除,它喜欢删除。

您可以像这样删除附加到logging的logging集合。

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

在该示例中,附加到订单的所有明细logging都将其状态设置为删除。 (准备添加更新的详细信息,作为订单更新的一部分)

我不知道为什么其他两个答案如此受欢迎!

我相信你是正确的,认为ORM框架应该处理它 – 毕竟,这是它所承诺的。 否则,您的域模型会受到持久性问题的影响。 如果您正确设置级联设置,NHibernate会很高兴地pipe理这个。 在entity framework中,也可能的是,他们只是希望在设置数据库模型时遵循更好的标准,尤其是当他们必须推断应该做什么级联时:

您必须使用“ 识别关系 ”来正确定义亲子 关系 。

如果你这样做,entity framework知道子对象是由父级标识的,因此它必须是“级联 – 删除孤儿”的情况。

除了上述,你可能需要(从NHibernate的经验)

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

而不是完全replace列表。

UPDATE

@ Slauma的评论提醒我,分离的实体是整体问题的另一部分。 为了解决这个问题,你可以采用使用自定义模型绑定器的方法,通过尝试从上下文中加载它来构build模型。 这篇博客文章展示了我的意思。

如果您在同一个类上使用entity framework的AutoMapper,则可能会遇到此问题。 例如,如果你的class级是

 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 

我只是有同样的错误。 我有两个具有父子关系的表,但是我在子表的表定义中的外键列上configuration了“on delete cascade”。 所以当我手动删除数据库中的父行(通过SQL)时,它会自动删除子行。

然而这在EF中不起作用,在这个线程中描述的错误出现了。 原因是,在我的实体数据模型(edmx文件)中,父表和子表之间的关联属性是不正确的。 End1 OnDelete选项被configuration为none (我的模型中的“End1”是具有多重1的结尾)。

我手动将End1 OnDelete选项更改为Cascade而不是工作。 我不知道为什么EF不能从这个数据库更新模型(我有一个数据库的第一个模型)来接受这个问题。

为了完整性,这是我的代码如何删除:

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

如果我没有级联删除定义,我将不得不手动删除父行之前删除子行。

我今天遇到这个问题,想分享我的解决scheme。 就我而言,解决scheme是在从数据库获取父项之前删除子项目。

以前我在做下面的代码。 然后我会得到这个问题中列出的同样的错误。

 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 

这是因为子实体被标记为已修改而不是已删除。

parent.Remove(child)被执行时,EF对Child Entity进行的修改仅仅是将其父对象的引用设置为null

在执行SaveChanges()之后发生exception时,您可以通过在Visual Studio的立即窗口中input以下代码来检查孩子的EntityState:

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

其中X应该被删除的实体replace。

如果您无权访问ObjectContext来执行_context.ChildEntity.Remove(child) ,则可以通过将外键作为子表的主键的一部分来解决此问题。

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

这样,如果执行parent.Remove(child) ,EF将正确标记实体为已删除。

您必须手动清除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); } } 

我已经尝试了这些解决scheme和许多其他的解决scheme,但是没有一个解决scheme。 由于这是谷歌的第一个答案,我会在这里添加我的解决scheme。

对我来说工作得很好的方法是在提交过程中将关系排除在外,因此EF没有任何事情搞砸了。 我通过在DBContext中重新find父对象并删除它。 由于重新find的对象的导航属性全部为空,因此在提交期间将忽略儿童的关系。

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

请注意,假定外键是使用ON DELETE CASCADE设置的,所以当父行被删除时,子项将被数据库清除。

这种types的解决scheme为我做了窍门:

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

重要的是要删除所有的logging并重新插入。 但对于我的情况(less于10),没关系。

我希望它有帮助。

我已经在几个小时之前遇到了这个问题,并尝试了一切,但在我的情况下,解决scheme是从上面列出的不同。

如果从数据库中使用已经获取的实体并尝试修改它的子节点,则会发生错误,但是如果从数据库中获得新的实体副本,则不会有任何问题。 不要使用这个:

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

用这个:

  public void CheckUsersCount(Guid companyProductId) { CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId); companyProduct.Name = "Test"; } 

出现这个问题是因为我们试图删除父表中仍存在的子表数据。 我们借助级联删除来解决问题。

在模型中创builddbcontext类的方法。

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

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

删除用于删除数据库中的logging列表的范围感谢

我使用Mosh的解决scheme ,但是我不明白如何正确地在代码中实现组合键。

所以这里是解决scheme:

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

我也用Mosh的答案解决了我的问题,我以为PeterB的答案有点用了,因为它使用了一个枚举作为外键。 请记住,添加此代码后,您需要添加新的迁移。

我也可以推荐这个博客文章的其他解决scheme:

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