对于使用代码首先EF 5测试版的应用程序,我有:
public class ParentObject
{
public int Id {get; set;}
public virtual List<ChildObject> ChildObjects {get; set;}
//Other members
}
和
public class ChildObject
{
public int Id {get; set;}
public int ParentObjectId {get; set;}
//Other members
}
相关的CRUD操作都是由库,必要时进行。
在
OnModelCreating(DbModelBuilder modelBuilder)
我已经建立起来:
modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
.WithOptional()
.HasForeignKey(c => c.ParentObjectId)
.WillCascadeOnDelete();
因此,如果一个ParentObject
被删除,它ChildObjects也有同感。
但是,如果我运行:
parentObject.ChildObjects.Clear();
_parentObjectRepository.SaveChanges(); //this repository uses the context
我得到异常:
操作失败:关系不能被改变,因为一个或多个外键的属性是不可为空。 当一个变化是有关系作出的相关外键属性设置为空值。 如果外键不支持空值,新的关系必须定义,外键属性必须指定一个非空值,或者无关的对象必须被删除。
这是有道理的作为实体的定义包括外键约束它正在被打破。
我可以对“明确自己打扮”,当它被孤立或我必须手动删除这些配置实体ChildObject
从上下文S(在这种情况下使用ChildObjectRepository)。
它实际上是支持的,但只有当您使用确定的关系 。 它的工作原理与代码首先为好。 你只需要定义复杂的按键为您ChildObject
同时包含Id
和ParentObjectId
:
modelBuilder.Entity<ChildObject>()
.HasKey(c => new {c.Id, c.ParentObjectId});
由于定义这样的键将删除默认的约定自动递增的ID,必须手动重新定义它:
modelBuilder.Entity<ChildObject>()
.Property(c => c.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
现在打电话来parentObject.ChildObjects.Clear()删除相关对象。
顺便说一句。 你的关系映射应该使用WithRequired
跟随自己真正的类,因为如果FK不是空的,它是不可选:
modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
.WithRequired()
.HasForeignKey(c => c.ParentObjectId)
.WillCascadeOnDelete();
更新:
我发现了一个方式,并不需要从孩子添加导航属性父实体或设立一个复杂的关键。
它基于此的文章 ,它使用ObjectStateManager
找到已删除的实体。
与列表ObjectStateEntry
在手,我们可以找到一个对EntityKey
每个,表示已删除的关系。
在这一点上,我找不到任何迹象,其中一个不得不被删除。 而且违背了本文的例子,简单地采摘第二个将获得在孩子有一个导航属性回父的情况下删除父。 因此,为了解决这个问题,我跟踪该类型应该与类处理OrphansToHandle
。
该模型:
public class ParentObject
{
public int Id { get; set; }
public virtual ICollection<ChildObject> ChildObjects { get; set; }
public ParentObject()
{
ChildObjects = new List<ChildObject>();
}
}
public class ChildObject
{
public int Id { get; set; }
}
其他类:
public class MyContext : DbContext
{
private readonly OrphansToHandle OrphansToHandle;
public DbSet<ParentObject> ParentObject { get; set; }
public MyContext()
{
OrphansToHandle = new OrphansToHandle();
OrphansToHandle.Add<ChildObject, ParentObject>();
}
public override int SaveChanges()
{
HandleOrphans();
return base.SaveChanges();
}
private void HandleOrphans()
{
var objectContext = ((IObjectContextAdapter)this).ObjectContext;
objectContext.DetectChanges();
var deletedThings = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).ToList();
foreach (var deletedThing in deletedThings)
{
if (deletedThing.IsRelationship)
{
var entityToDelete = IdentifyEntityToDelete(objectContext, deletedThing);
if (entityToDelete != null)
{
objectContext.DeleteObject(entityToDelete);
}
}
}
}
private object IdentifyEntityToDelete(ObjectContext objectContext, ObjectStateEntry deletedThing)
{
// The order is not guaranteed, we have to find which one has to be deleted
var entityKeyOne = objectContext.GetObjectByKey((EntityKey)deletedThing.OriginalValues[0]);
var entityKeyTwo = objectContext.GetObjectByKey((EntityKey)deletedThing.OriginalValues[1]);
foreach (var item in OrphansToHandle.List)
{
if (IsInstanceOf(entityKeyOne, item.ChildToDelete) && IsInstanceOf(entityKeyTwo, item.Parent))
{
return entityKeyOne;
}
if (IsInstanceOf(entityKeyOne, item.Parent) && IsInstanceOf(entityKeyTwo, item.ChildToDelete))
{
return entityKeyTwo;
}
}
return null;
}
private bool IsInstanceOf(object obj, Type type)
{
// Sometimes it's a plain class, sometimes it's a DynamicProxy, we check for both.
return
type == obj.GetType() ||
(
obj.GetType().Namespace == "System.Data.Entity.DynamicProxies" &&
type == obj.GetType().BaseType
);
}
}
public class OrphansToHandle
{
public IList<EntityPairDto> List { get; private set; }
public OrphansToHandle()
{
List = new List<EntityPairDto>();
}
public void Add<TChildObjectToDelete, TParentObject>()
{
List.Add(new EntityPairDto() { ChildToDelete = typeof(TChildObjectToDelete), Parent = typeof(TParentObject) });
}
}
public class EntityPairDto
{
public Type ChildToDelete { get; set; }
public Type Parent { get; set; }
}
原来的答案
为了解决没有设置复杂的关键这个问题,你可以重写SaveChanges
你的DbContext
,但随后用ChangeTracker
以避免为了找到孤立对象访问数据库。
首先导航属性添加到ChildObject
(你可以保持int ParentObjectId
财产,如果你愿意,它仍然可以正常工作):
public class ParentObject
{
public int Id { get; set; }
public virtual List<ChildObject> ChildObjects { get; set; }
}
public class ChildObject
{
public int Id { get; set; }
public virtual ParentObject ParentObject { get; set; }
}
然后寻找使用孤立对象ChangeTracker
:
public class MyContext : DbContext
{
//...
public override int SaveChanges()
{
HandleOrphans();
return base.SaveChanges();
}
private void HandleOrphans()
{
var orphanedEntities =
ChangeTracker.Entries()
.Where(x => x.Entity.GetType().BaseType == typeof(ChildObject))
.Select(x => ((ChildObject)x.Entity))
.Where(x => x.ParentObject == null)
.ToList();
Set<ChildObject>().RemoveRange(orphanedEntities);
}
}
你的配置变为:
modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
.WithRequired(c => c.ParentObject)
.WillCascadeOnDelete();
我做了一个简单的速度测试迭代10.000倍。 随着HandleOrphans()
启用花了1:01.443分钟来完成,它禁用它是0:59.326分(均为三次运行的平均值)。 下面的测试代码。
using (var context = new MyContext())
{
var parentObject = context.ParentObject.Find(1);
parentObject.ChildObjects.Add(new ChildObject());
context.SaveChanges();
}
using (var context = new MyContext())
{
var parentObject = context.ParentObject.Find(1);
parentObject.ChildObjects.Clear();
context.SaveChanges();
}
这是不是被自动EF现在支持。 您可以在上下文的SaveChanges覆盖和手动删除不再有父的子对象做到这一点。 该代码将是这样的:
public override int SaveChanges()
{
foreach (var bar in Bars.Local.ToList())
{
if (bar.Foo == null)
{
Bars.Remove(bar);
}
}
return base.SaveChanges();
}
文章来源: Can EF automatically delete data that is orphaned, where the parent is not deleted?