EF是否可以自动删除孤立的数据,而不删除父数据?

对于使用Code First EF 5 beta的应用程序,我有:

public class ParentObject { public int Id {get; set;} public virtual List 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().HasMany(p => p.ChildObjects) .WithOptional() .HasForeignKey(c => c.ParentObjectId) .WillCascadeOnDelete(); 

因此,如果删除ParentObject,它的ChildObjects也是如此。

但是,如果我跑:

 parentObject.ChildObjects.Clear(); _parentObjectRepository.SaveChanges(); //this repository uses the context 

我得到了例外:

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

这是有道理的,因为实体的定义包括正在被破坏的外键约束。

我可以将实体配置为在孤立时“清除自己”,或者我必须从上下文中手动删除这些ChildObject (在本例中使用ChildObjectRepository)。

它实际上是受支持的,但仅限于使用识别关系时 。 它首先与代码一起使用。 您只需要为包含IdParentObjectId定义复杂键:

 modelBuilder.Entity() .HasKey(c => new {c.Id, c.ParentObjectId}); 

因为定义此类键将删除自动递增的Id的默认约定,您必须手动重新定义它:

 modelBuilder.Entity() .Property(c => c.Id) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); 

现在调用parentObject.ChildObjects.Clear()删除依赖对象。

顺便说一句。 您的关系映射应使用WithRequired来跟踪您的真实类,因为如果FK不可为空,则它不是可选的:

 modelBuilder.Entity().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 ChildObjects { get; set; } public ParentObject() { ChildObjects = new List(); } } public class ChildObject { public int Id { get; set; } } 

其他课程:

 public class MyContext : DbContext { private readonly OrphansToHandle OrphansToHandle; public DbSet ParentObject { get; set; } public MyContext() { OrphansToHandle = new OrphansToHandle(); OrphansToHandle.Add(); } 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 List { get; private set; } public OrphansToHandle() { List = new List(); } public void Add() { List.Add(new EntityPairDto() { ChildToDelete = typeof(TChildObjectToDelete), Parent = typeof(TParentObject) }); } } public class EntityPairDto { public Type ChildToDelete { get; set; } public Type Parent { get; set; } } 

原始答案

要在不设置复杂密钥的情况下解决此问题,可以覆盖DbContextSaveChanges ,但随后使用ChangeTracker来避免访问数据库以查找孤立对象。

首先向ChildObject添加一个导航属性(如果需要,可以保留int ParentObjectId属性,它可以以任何方式工作):

 public class ParentObject { public int Id { get; set; } public virtual List 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().RemoveRange(orphanedEntities); } } 

您的配置变为:

 modelBuilder.Entity().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(); }