我得到这个错误时,我GetById()在一个实体,然后设置子实体的集合到我的新列表,来自MVC视图。
操作失败
关系是无法改变的
因为一个或多个外键
Properties是非空的。当一个
关系发生了变化
相关外键属性设置为
空值。如果外键是
不支持空值,新建
关系必须被定义
必须分配外键属性
另一个非空值或
必须删除不相关的对象。
我不太理解这句话:
这种关系无法改变
因为一个或多个外键
Properties是非空的。
我为什么要改变两个实体之间的关系?它应该在整个应用程序的生命周期内保持不变。
发生异常的代码只是简单地将集合中修改过的子类分配给现有的父类。这将有望满足取消子类,增加新的和修改。我本以为实体框架处理这个。
代码行可以提炼为:
var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
我不知道为什么其他两个答案这么受欢迎!
我相信您认为ORM框架应该处理它是正确的——毕竟,这是它承诺交付的。否则,您的域模型就会被持久性问题所破坏。如果你正确地设置了级联设置,NHibernate就能很好地管理它。在实体框架中也有可能,他们只是希望你在建立数据库模型时遵循更好的标准,特别是当他们不得不推断应该做什么级联时:
您必须使用“识别关系”来正确地定义父-子关系。
如果你这样做,实体框架知道子对象是由父对象标识的,因此它必须是一个“级联删除孤儿”的情况。
除了上面的,你可能需要(从NHibernate的经验)
thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);
而不是完全替换列表。
更新
@Slauma的评论提醒我,分离实体是整体问题的另一部分。为了解决这个问题,您可以采用使用自定义模型绑定器的方法,通过尝试从上下文加载模型来构造模型。这篇博客文章展示了我的意思。
如果你在同一个类上使用AutoMapper和实体框架,你可能会遇到这个问题。例如,如果你的类是
class A
{
public ClassB ClassB { get; set; }
public int ClassBId { get; set; }
}
AutoMapper.Map<A, A>(input, destination);
这将尝试复制两个属性。在这种情况下,ClassBId是非空的。因为AutoMapper将复制目标。ClassB = input.ClassB;这将导致一个问题。
将您的AutoMapper设置为Ignore ClassB属性。
cfg.CreateMap<A, A>()
.ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
我也遇到了同样的问题,但我知道它在其他情况下也能正常工作,所以我把问题简化为:
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()
OtherRelatedItems有一个复合主键(parentId +一些本地列),工作正常
probleaticitems有自己的单列主键,而parentId只是一个FK。这导致了Clear()之后的异常。
我所要做的就是使ParentId成为复合PK的一部分,以表明没有父元素就不能存在子元素。我使用DB-first模型,添加PK并将parentId列标记为EntityKey(因此,我必须在DB和EF中更新它-不确定EF单独是否足够)。
仔细想想,这是一个非常优雅的区别,EF使用它来决定没有父对象的子对象是否“有意义”(在这种情况下,Clear()不会删除它们并抛出异常,除非你将ParentId设置为其他/特殊的对象),或者-就像最初的问题一样-我们期望项一旦从父对象中删除就会删除。
我不知道为什么其他两个答案这么受欢迎!
我相信您认为ORM框架应该处理它是正确的——毕竟,这是它承诺交付的。否则,您的域模型就会被持久性问题所破坏。如果你正确地设置了级联设置,NHibernate就能很好地管理它。在实体框架中也有可能,他们只是希望你在建立数据库模型时遵循更好的标准,特别是当他们不得不推断应该做什么级联时:
您必须使用“识别关系”来正确地定义父-子关系。
如果你这样做,实体框架知道子对象是由父对象标识的,因此它必须是一个“级联删除孤儿”的情况。
除了上面的,你可能需要(从NHibernate的经验)
thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);
而不是完全替换列表。
更新
@Slauma的评论提醒我,分离实体是整体问题的另一部分。为了解决这个问题,您可以采用使用自定义模型绑定器的方法,通过尝试从上下文加载模型来构造模型。这篇博客文章展示了我的意思。
这是一个很大的问题。在你的代码中实际发生的是这样的:
从数据库加载Parent并获得附加实体
您用分离的子集合替换它的子集合
您保存更改,但在此操作期间,所有的子元素都被认为是添加的,因为EF直到此时才知道它们。因此EF尝试将null设置为旧子节点的外键,并插入所有新的子节点=>重复行。
解决方法取决于你想做什么以及你想怎么做?
如果您正在使用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();