C# EF是否可以自动删除孤立的数据,而父项未被删除?

C# EF是否可以自动删除孤立的数据,而父项未被删除?,c#,entity-framework,entity-framework-5,C#,Entity Framework,Entity Framework 5,对于使用Code First EF 5 beta版的应用程序,我有: public class ParentObject { public int Id {get; set;} public virtual List<ChildObject> ChildObjects {get; set;} //Other members } 必要时,相关CRUD操作由存储库执行 在 我已经建立了: modelBuilder.Entity<ParentObject&g

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

public class ParentObject
{
    public int Id {get; set;}
    public virtual List<ChildObject> ChildObjects {get; set;}
    //Other members
}
必要时,相关CRUD操作由存储库执行

我已经建立了:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)
            .WithOptional()
            .HasForeignKey(c => c.ParentObjectId)
            .WillCascadeOnDelete();
我得到一个例外:

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

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


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

EF现在不自动支持这一点。您可以通过覆盖上下文中的SaveChanges并手动删除不再具有父对象的子对象来实现。代码如下所示:

public override int SaveChanges()
{
    foreach (var bar in Bars.Local.ToList())
    {
        if (bar.Foo == null)
        {
            Bars.Remove(bar);
        }
    }

    return base.SaveChanges();
}

它实际上是受支持的,但仅当您使用时。它也首先与代码一起工作。您只需要为同时包含
Id
ParentObjectId
ChildObject
定义复杂键:

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

更新:

我找到了一种不需要将导航属性从子实体添加到父实体或设置复杂键的方法

它基于此,使用
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; }
}
然后使用
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);
    }
}

想分享另一个为我工作的.NETEF核心解决方案,可能有人会发现它很有用

我有一个带有两个外键(非此即彼)的子表,所以接受的解决方案对我不起作用。根据Marcos Dimitrio的回答,我得出以下结论:

在我的自定义数据库上下文中:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
  {
    var modifiedEntities = this.ChangeTracker.Entries().Where(c => c.State == EntityState.Modified);
    foreach (var entityEntry in modifiedEntities)
    {
      if (entityEntry.Entity is ChildObject)
      {
         var fkProperty = entityEntry.Property(nameof(ChildObject.ParentObjectId));
         if (fkProperty.IsModified && fkProperty.CurrentValue == null && fkProperty.OriginalValue != null)
         {
           // Checked if FK was set to NULL
           entityEntry.State = EntityState.Deleted;
         }
      }
    }

    return await base.SaveChangesAsync(cancellationToken);
  }
public override异步任务savechangesync(CancellationToken CancellationToken=new CancellationToken())
{
var modifiedEntities=this.ChangeTracker.Entries(),其中(c=>c.State==EntityState.Modified);
foreach(ModifiedEntity中的变量entityEntry)
{
if(entityEntry.Entity是ChildObject)
{
var fkProperty=entityEntry.Property(nameof(ChildObject.ParentObjectId));
如果(fkProperty.IsModified&&fkProperty.CurrentValue==null&&fkProperty.OriginalValue!=null)
{
//检查FK是否设置为空
entityEntry.State=EntityState.Deleted;
}
}
}
返回wait base.saveChangesSync(cancellationToken);
}

在EF Core中,可以通过以下方式完成

像这样:

dbContext.Children.Clear();

对。EF Core中的以下工作:

确保将级联行为设置为
cascade
,如下所示:

entity.HasOne(d => d.Parent)
                    .WithMany(p => p.Children)
                    .HasForeignKey(d => d.ParentId)
                    .OnDelete(DeleteBehavior.Cascade);
然后在所有要删除的子实体中将父属性设置为空,如下所示:

var childrenToBeRemoved = parent.Children.Where(filter);
foreach(var child in childrenToBeRemoved)
{
    child.Parent = null;
}

现在,
context.SaveAsync()
应该删除所有孤立子实体。

谢天谢地,EF团队可能会提出一个内置解决方案,不需要修改internals@Ladislav那么单向的情况呢?当ChildObject不知道父对象时?@Ladislav我正在使用EF6,并尝试像您在示例中所做的那样。但是,调用Collection.Clear()和AddOrUpdate(父级)似乎不起作用。有什么我必须做的吗?@Jamez-结果是我打电话给clear的时候没有给孩子们的收藏品补水。加载子对象解决了这个问题。我看到的许多解决这个问题的示例都没有使用对象引用。在这段代码中,您可以创建外键,因为父属性是int“ParentObjectId”。不幸的是,如果您使用的是完整的OO对象引用,那么它不起作用,因为您无法使用引用定义外键。这给我留下了同样的问题,孤儿不会被删除。拯救了我的一天!“DatabaseGenerateOption.Identity”的代码未包含在有关此问题的其他帖子中,并且Identity OFF阻止创建新的/替换的集合项。如果不针对每个SaveChanges()执行此代码,请参见您现在在MS on EF工作的情况。。。你有更合理的解决办法吗?这段代码“有效”,但永远不会在真正的代码库中运行。EF6支持实体引用的这个用例吗?你测试过吗?@GertArnold是的。这种方法有什么问题吗?嗯,我想这取决于需要
ParentId
,以及查询
childrenToBeRemoved
的方式。也许你应该详细说明一下。除此之外,我不推荐这种删除记录的方法。这是EF特有的,一点也不直观。我希望EF不要这样做。根据Microsoft文档,这也适用于不可为空的
ParentId
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; }
}
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; }
}
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();
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();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
  {
    var modifiedEntities = this.ChangeTracker.Entries().Where(c => c.State == EntityState.Modified);
    foreach (var entityEntry in modifiedEntities)
    {
      if (entityEntry.Entity is ChildObject)
      {
         var fkProperty = entityEntry.Property(nameof(ChildObject.ParentObjectId));
         if (fkProperty.IsModified && fkProperty.CurrentValue == null && fkProperty.OriginalValue != null)
         {
           // Checked if FK was set to NULL
           entityEntry.State = EntityState.Deleted;
         }
      }
    }

    return await base.SaveChangesAsync(cancellationToken);
  }
dbContext.Children.Clear();
entity.HasOne(d => d.Parent)
                    .WithMany(p => p.Children)
                    .HasForeignKey(d => d.ParentId)
                    .OnDelete(DeleteBehavior.Cascade);
var childrenToBeRemoved = parent.Children.Where(filter);
foreach(var child in childrenToBeRemoved)
{
    child.Parent = null;
}