我得到这个错误时,我GetById()在一个实体,然后设置子实体的集合到我的新列表,来自MVC视图。
操作失败
关系是无法改变的
因为一个或多个外键
Properties是非空的。当一个
关系发生了变化
相关外键属性设置为
空值。如果外键是
不支持空值,新建
关系必须被定义
必须分配外键属性
另一个非空值或
必须删除不相关的对象。
我不太理解这句话:
这种关系无法改变
因为一个或多个外键
Properties是非空的。
我为什么要改变两个实体之间的关系?它应该在整个应用程序的生命周期内保持不变。
发生异常的代码只是简单地将集合中修改过的子类分配给现有的父类。这将有望满足取消子类,增加新的和修改。我本以为实体框架处理这个。
代码行可以提炼为:
var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
如果你在同一个类上使用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
这是因为子实体被标记为Modified而不是Deleted。
当执行parent. remove (Child)时,EF对子实体所做的修改只是将其父实体的引用设置为null。
当异常发生时,在执行SaveChanges()后,你可以通过在Visual Studio的即时窗口中输入以下代码来检查子对象的EntityState:
_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将正确地将实体标记为已删除。
我也遇到了同样的问题,但我知道它在其他情况下也能正常工作,所以我把问题简化为:
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的评论提醒我,分离实体是整体问题的另一部分。为了解决这个问题,您可以采用使用自定义模型绑定器的方法,通过尝试从上下文加载模型来构造模型。这篇博客文章展示了我的意思。
你必须手动清除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);
}
}