当我在实体上使用GetById()然后将子实体的集合设置为来自MVC视图的新列表时,我收到此错误。
操作失败: 关系无法改变 因为一个或多个外键 属性不可为空。当一个 变化是一种关系, 相关的外键属性设置为 空值。如果是外键的话 不支持空值,一个新的 关系必须定义, 必须分配外键属性 另一个非空值,或者 必须删除不相关的对象。
我不太明白这一行:
这种关系无法改变 因为一个或多个外键 属性是不可为空的。
为什么要更改2个实体之间的关系?它应该在整个应用程序的整个生命周期内保持不变。
发生异常的代码很简单,即将集合中已修改的子类分配给现有父类。这有望满足删除子类,添加新类和修改的需要。我原以为Entity Framework会处理这个问题。
代码行可以提炼为:
var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
答案 0 :(得分:146)
您应手动逐个删除旧子项thisParent.ChildItems
。实体框架不会为您做到这一点。它最终无法决定你想要对旧的子项目做什么 - 如果你想要扔掉它们或者你想保留它们并将它们分配给其他父实体。您必须告诉实体框架您的决定。但是,您必须做出这两个决定中的一个,因为子实体不能在没有引用数据库中的任何父项的情况下独自生活(由于外键约束)。这基本上就是异常所说的。
修改强>
如果可以添加,更新和删除子项目,我会怎么做:
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
然后代码看起来有点不同。)我还删除了所有存储库抽象以保持简单。
我不知道这是否是一个很好的解决方案,但我相信必须在这些方面做一些艰苦的工作来处理导航集合中的各种变化。我也很高兴看到一种更简单的方法。
答案 1 :(得分:90)
您遇到此问题的原因是合成与聚合之间存在差异。
在合成中,子对象在创建父对象时创建,在其父对象被销毁时被销毁。因此它的生命周期由其父母控制。例如博客文章及其评论。如果删除帖子,则应删除其评论。对不存在的帖子发表评论是没有意义的。订单和订单商品也是如此。
在聚合中,子对象可以存在,而不管其父对象。如果父对象被销毁,则子对象仍然可以存在,因为它可能会在以后添加到其他父对象。例如:播放列表与该播放列表中的歌曲之间的关系。如果删除播放列表,则不应删除歌曲。它们可能会被添加到不同的播放列表中。
实体框架区分聚合和组合关系的方式如下:
对于组合:它期望子对象具有复合主键(ParentID,ChildID)。这是设计的,因为孩子的ID应该在他们父母的范围内。
对于聚合:它期望子对象中的外键属性可以为空。
因此,您遇到此问题的原因是您在子表中设置主键的原因。它应该是复合的,但事实并非如此。因此,实体框架将此关联视为聚合,这意味着,当您删除或清除子对象时,它不会删除子记录。它只是删除关联并将相应的外键列设置为NULL(因此这些子记录以后可以与不同的父关联)。由于您的列不允许NULL,因此您将获得您提到的异常。
<强>解决方案:强>
1-如果您有充分理由不想使用复合键,则需要显式删除子对象。这可以比之前建议的解决方案更简单:
context.Children.RemoveRange(parent.Children);
2-否则,通过在子表上设置正确的主键,您的代码看起来会更有意义:
parent.Children.Clear();
答案 2 :(得分:68)
这是一个非常大的问题。您的代码中实际发生的是:
Parent
并获取附加实体现在解决方案真的取决于你想做什么以及你想怎么做?
如果您使用的是ASP.NET MVC,可以尝试使用UpdateModel or 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();
答案 3 :(得分:36)
我发现this回答对同一错误更有帮助。 当您删除时,EF似乎不喜欢它,它更喜欢删除。
您可以删除附加到此类记录的记录集合。
order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);
在该示例中,附加到订单的所有详细记录都将其状态设置为“删除”。 (准备添加更新的详细信息,作为订单更新的一部分)
答案 4 :(得分:19)
我不知道为什么其他两个答案如此受欢迎!
我相信你是正确的,假设ORM框架应该处理它 - 毕竟,这是它承诺提供的。否则,您的域模型会因持久性问题而损坏。如果正确设置级联设置,NHibernate会愉快地管理它。在实体框架中,它们也有可能,它们只是希望您在设置数据库模型时遵循更好的标准,特别是当他们必须推断应该进行的级联时:
您必须使用“define the parent - child relationship”正确identifying relationship。
如果这样做,Entity Framework知道父对象标识的子对象,因此它必须是“级联删除孤立”情况。
除此之外,您可能需要(来自NHibernate经验)
thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);
而不是完全替换列表。
<强>更新强>
@Slauma的评论提醒我,分离的实体是整体问题的另一部分。要解决这个问题,您可以采用自定义模型绑定器的方法,通过尝试从上下文加载模型来构建模型。 This blog post显示了我的意思。
答案 5 :(得分:9)
如果您在同一个类上使用带有Entity Framework的AutoMapper,则可能会遇到此问题。例如,如果你的班级是
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
答案 6 :(得分:4)
这是因为Child Entity被标记为Modified而不是Deleted。
EF执行parent.Remove(child)
时对子实体的修改只是将对其父级的引用设置为null
。
您可以在执行SaveChanges()
之后,在发生异常时在Visual Studio的立即窗口中键入以下代码来检查子实体状态:
_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会正确地将实体标记为已删除。
答案 7 :(得分:3)
我遇到了同样的问题,但是我知道它在其他情况下也可以正常工作,因此我将问题归结为:
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()
我要做的就是使ParentId成为复合PK的一部分,以指示没有父母就不能存在子代。我使用数据库优先模型,添加了PK ,并将parentId列标记为EntityKey (因此,我不得不在数据库和EF中都对其进行更新-不确定仅使用EF是否足够)。>
考虑一下,这是EF用来判断子代是否在没有父代的情况下“有意义”的非常优雅的区分(在这种情况下,Clear()不会删除它们并抛出异常,除非您将ParentId设置为某种值) else / special),或者-就像在原始问题中一样-我们希望将这些项目从父项中删除后再删除。
答案 8 :(得分:3)
我遇到了同样的错误。 我有两个父子关系表,但我在子表的表定义中的外键列上配置了“on delete cascade”。 因此,当我手动删除数据库中的父行(通过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();
}
如果我没有定义级联删除,我必须在删除父行之前手动删除子行。
答案 9 :(得分: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);
}
}
答案 10 :(得分:2)
我今天遇到了这个问题,想分享我的解决方案。就我而言,解决方案是在从数据库获取Parent之前删除子项。
以前我在下面的代码中这样做。然后我会得到这个问题中列出的相同错误。
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
答案 11 :(得分:1)
这种解决方案对我有用:
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)就可以了。
我希望它有所帮助。
答案 12 :(得分:1)
我使用了Mosh's solution,但对我来说,如何在代码中正确实现组合键并不明显。
所以这是解决方案:
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; }
}
答案 13 :(得分:1)
我已经尝试过这些解决方案以及其他许多解决方案,但它们都没有完成。由于这是谷歌的第一个答案,我将在此处添加我的解决方案。
对我来说效果很好的方法是在提交期间将关系从图片中删除,因此EF无法搞砸。我通过在DBContext中重新查找父对象并删除它来完成此操作。由于重新找到的对象的导航属性都为null,因此在提交期间将忽略子项的关系。
var toDelete = db.Parents.Find(parentObject.ID);
db.Parents.Remove(toDelete);
db.SaveChanges();
请注意,这假设外键是使用ON DELETE CASCADE设置的,因此当删除父行时,数据库将清除子项。
答案 14 :(得分:0)
我在几个小时之前遇到了这个问题并尝试了一切,但就我而言,解决方案与上面列出的不同。
如果您使用已经从数据库中检索过的实体并尝试修改它的子项,则会出现错误,但如果您从数据库中获得实体的新副本,则不应出现任何问题。 不要用这个:
public void CheckUsersCount(CompanyProduct companyProduct)
{
companyProduct.Name = "Test";
}
使用此:
public void CheckUsersCount(Guid companyProductId)
{
CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
companyProduct.Name = "Test";
}
答案 15 :(得分: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();
级联删除选项使用此简单代码删除父级以及父级相关子表。以这种简单的方式尝试。
删除用于删除数据库中记录列表的范围 感谢
答案 16 :(得分:0)
我也用Mosh's answer解决了我的问题,我认为PeterB's answer有点因为它使用枚举作为外键。请记住,添加此代码后,您需要添加新的迁移。
我还可以推荐此博客文章以获取其他解决方案:
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; }
}
答案 17 :(得分: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);
答案 18 :(得分:0)
当我要删除我的记录时遇到了同样的问题,因为出现此问题,因此此问题的解决方案是,当您要删除记录时,比在删除标题/主记录之前丢失一些东西您必须写在头文件/主文件之前编写代码以删除其详细信息,希望您的问题能得到解决。
答案 19 :(得分:0)
如果您使用的是自动映射器,并且遇到以下问题是很好的解决方案,那么它对我有用
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();